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.server.stats; 018 019import static java.util.Comparator.*; 020import static java.util.stream.Collectors.toSet; 021import static org.apache.juneau.commons.utils.CollectionUtils.*; 022import static org.apache.juneau.commons.utils.ObjectUtils.*; 023import static org.apache.juneau.commons.utils.Shorts.*; 024 025import java.time.*; 026import java.util.*; 027import java.util.concurrent.*; 028 029import org.apache.juneau.commons.inject.*; 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 /** 044 * Builder class. 045 */ 046 public static class Builder { 047 048 final BeanStore beanStore; 049 ThrownStore parent; 050 Class<? extends ThrownStats> statsImplClass; 051 Set<Class<?>> ignoreClasses; 052 Clock clock; 053 054 private ThrownStore impl; 055 private Class<? extends ThrownStore> type; 056 057 /** 058 * Constructor. 059 * 060 * @param beanStore The bean store to use for creating beans. 061 */ 062 protected Builder(BeanStore beanStore) { 063 this.beanStore = beanStore; 064 } 065 066 /** 067 * Returns the bean store. 068 * 069 * @return The bean store. 070 */ 071 public BeanStore beanStore() { 072 return beanStore; 073 } 074 075 /** 076 * Specifies the clock used to stamp the first and last occurrence times of recorded exceptions. 077 * 078 * <p> 079 * Defaults to the parent store's clock, or {@link Clock#systemUTC()} if there is no parent. Tests can pass 080 * {@link Clock#fixed(Instant, ZoneId)} to make the occurrence times reported by {@link ThrownStats} deterministic. 081 * 082 * @param value The new value for this setting. Can be <jk>null</jk> (inherits the default). 083 * @return This object. 084 */ 085 public Builder clock(Clock value) { 086 clock = value; 087 return this; 088 } 089 090 /** 091 * Specifies the list of classes to ignore when calculating stack traces. 092 * 093 * <p> 094 * Stack trace elements that are the specified class will be ignored. 095 * 096 * @param value The list of classes to ignore. 097 * @return This object. 098 */ 099 public Builder ignoreClasses(Class<?>...value) { 100 this.ignoreClasses = set(value); 101 return this; 102 } 103 104 /** 105 * Overrides the bean returned by the {@link #build()} method with a pre-instantiated instance. 106 * 107 * @param value The setting value. Can be <jk>null</jk> (the {@link #build()} method instantiates the {@link #type(Class) type} instead). 108 * @return This object. 109 */ 110 public Builder impl(Object value) { 111 impl = (ThrownStore) value; 112 return this; 113 } 114 115 /** 116 * Specifies the parent store of this store. 117 * 118 * <p> 119 * Parent stores are used for aggregating statistics across multiple child stores. 120 * <br>The {@link ThrownStore#GLOBAL} store can be used for aggregating all thrown exceptions in a single JVM. 121 * 122 * @param value The parent store. Can be <jk>null</jk> for a root store with no parent chain. 123 * @return This object. 124 */ 125 public Builder parent(ThrownStore value) { 126 parent = value; 127 return this; 128 } 129 130 /** 131 * Specifies a subclass of {@link ThrownStats} to use for individual method statistics. 132 * 133 * @param value The new value for this setting. 134 * @return This object. 135 */ 136 public Builder statsImplClass(Class<? extends ThrownStats> value) { 137 statsImplClass = value; 138 return this; 139 } 140 141 /** 142 * Overrides the bean type produced by the {@link #build()} method. 143 * 144 * @param value The setting value. Can be <jk>null</jk> (defaults to {@link ThrownStore}). 145 * @return This object. 146 */ 147 @SuppressWarnings({ 148 "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site 149 }) 150 public Builder type(Class<?> value) { 151 type = (Class<? extends ThrownStore>) value; 152 return this; 153 } 154 155 /** 156 * Creates the bean. 157 * 158 * @return A new bean. 159 */ 160 public ThrownStore build() { 161 if (nn(impl)) 162 return impl; 163 if (type == null || type == ThrownStore.class) 164 return new ThrownStore(this); 165 return BeanInstantiator.of(ThrownStore.class, beanStore) 166 .type(type) 167 .noBuilder() 168 .addBean(Builder.class, this) 169 .run(); 170 } 171 } 172 173 /** Identifies a single global store for the entire JVM. */ 174 public static final ThrownStore GLOBAL = new ThrownStore(); 175 176 /** 177 * Static creator. 178 * 179 * @return A new builder for this object. 180 */ 181 public static Builder create() { 182 return new Builder(BasicBeanStore.INSTANCE); 183 } 184 185 /** 186 * Static creator. 187 * 188 * @param beanStore The bean store to use for creating beans. 189 * @return A new builder for this object. 190 */ 191 public static Builder create(BeanStore beanStore) { 192 return new Builder(beanStore); 193 } 194 195 private final ConcurrentHashMap<Long,ThrownStats> db = new ConcurrentHashMap<>(); 196 private final Optional<ThrownStore> parent; 197 private final BeanStore beanStore; 198 private final Class<? extends ThrownStats> statsImplClass; 199 private final Set<String> ignoreClasses; 200 private final Clock clock; 201 202 /** 203 * Constructor. 204 */ 205 public ThrownStore() { 206 this(create(BasicBeanStore.INSTANCE)); 207 } 208 209 /** 210 * Constructor. 211 * 212 * @param builder The builder for this object. Must not be <jk>null</jk>. 213 */ 214 public ThrownStore(Builder builder) { 215 this.parent = o(builder.parent); 216 this.beanStore = builder.beanStore(); 217 218 this.statsImplClass = coalesce(builder.statsImplClass, parent.isPresent() ? parent.get().statsImplClass : null, null); 219 this.clock = coalesce(builder.clock, parent.isPresent() ? parent.get().clock : null, Clock.systemUTC()); 220 221 Set<String> s = null; 222 if (nn(builder.ignoreClasses)) 223 s = builder.ignoreClasses.stream().map(Class::getName).collect(toSet()); 224 if (s == null && parent.isPresent()) 225 s = parent.get().ignoreClasses; 226 if (s == null) 227 s = Collections.emptySet(); 228 this.ignoreClasses = u(s); 229 } 230 231 /** 232 * Adds the specified thrown exception to this database. 233 * 234 * @param e The exception to add. 235 * @return This object. 236 */ 237 @SuppressWarnings({ 238 "javabugs:S2259" // The exception must not be null, per the Javadoc contract; find() returns null only for a null input. 239 }) 240 public ThrownStats add(Throwable e) { 241 ThrownStats s = find(e); 242 s.increment(); 243 parent.ifPresent(x -> x.add(e)); 244 return s; 245 } 246 247 /** 248 * Returns the list of all stack traces in this database. 249 * 250 * @return The list of all stack traces in this database, cloned and sorted by count descending. 251 */ 252 public List<ThrownStats> getStats() { return db.values().stream().map(ThrownStats::copyOrNull).sorted(comparingInt(ThrownStats::getCount).reversed()).toList(); } 253 254 /** 255 * Retrieves the stack trace information for the exception with the specified hash as calculated by {@link #hash(Throwable)}. 256 * 257 * @param hash The hash of the exception. 258 * @return A clone of the stack trace info, never <jk>null</jk>. 259 */ 260 public Optional<ThrownStats> getStats(long hash) { 261 ThrownStats s = db.get(hash); 262 return o(ThrownStats.copyOrNull(s)); 263 } 264 265 /** 266 * Retrieves the stats for the specified thrown exception. 267 * 268 * @param e The exception. Must not be <jk>null</jk>. 269 * @return A clone of the stats, never <jk>null</jk>. 270 */ 271 public Optional<ThrownStats> getStats(Throwable e) { 272 return getStats(hash(e)); 273 } 274 275 /** 276 * Clears out the stack trace cache. 277 */ 278 public void reset() { 279 db.clear(); 280 } 281 282 private ThrownStats find(Throwable t) { 283 284 if (t == null) 285 return null; 286 287 long hash = hash(t); 288 289 ThrownStats stc = db.get(hash); 290 if (stc == null) { 291 // @formatter:off 292 stc = ThrownStats 293 .create(beanStore) 294 .type(statsImplClass) 295 .clock(clock) 296 .throwable(t) 297 .hash(hash) 298 .stackTrace(createStackTrace(t)) 299 .causedBy(find(t.getCause())) 300 .build(); 301 // @formatter:on 302 303 db.putIfAbsent(hash, stc); 304 stc = db.get(hash); 305 } 306 307 return stc; 308 } 309 310 /** 311 * Converts the stack trace for the specified throwable into a simple list of strings. 312 * 313 * <p> 314 * The stack trace elements for the throwable are sent through {@link #normalize(StackTraceElement)} to convert 315 * them to simple strings. 316 * 317 * 318 * @param t The throwable to create the stack trace for. Must not be <jk>null</jk>. 319 * @return A modifiable list of strings. 320 */ 321 protected List<String> createStackTrace(Throwable t) { 322 return l(t.getStackTrace()).stream().filter(this::include).map(this::normalize).toList(); 323 } 324 325 /** 326 * Calculates a 32-bit hash for the specified throwable based on the stack trace generated by {@link #createStackTrace(Throwable)}. 327 * 328 * <p> 329 * Subclasses can override this method to provide their own implementation. 330 * 331 * @param t The throwable to calculate the stack trace on. Must not be <jk>null</jk>. 332 * @return A calculated hash. 333 */ 334 protected long hash(Throwable t) { 335 long h = 1125899906842597L; // prime 336 for (var s : createStackTrace(t)) { 337 var len = s.length(); 338 for (var i = 0; i < len; i++) 339 h = 31 * h + s.charAt(i); 340 } 341 return h; 342 } 343 344 /** 345 * Returns <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}. 346 * 347 * @param e The stack trace element. 348 * @return <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}. 349 */ 350 protected boolean include(StackTraceElement e) { 351 return true; 352 } 353 354 /** 355 * Converts the specified stack trace element into a normalized string. 356 * 357 * <p> 358 * The default implementation simply replaces <js>"\\$.*"</js> with <js>"..."</js> which should take care of stuff like stack 359 * trace elements of lambda expressions. 360 * 361 * @param e The stack trace element to convert. Must not be <jk>null</jk>. 362 * @return The converted stack trace element. 363 */ 364 protected String normalize(StackTraceElement e) { 365 if (ignoreClasses.contains(e.getClassName())) 366 return "<ignored>"; 367 var s = e.toString(); 368 var i = s.indexOf('$'); 369 if (i == -1) 370 return s; 371 var j = s.indexOf('(', i); 372 if (j == -1) 373 return s; // Probably can't happen. 374 var s2 = s.substring(0, i); 375 var s3 = s.substring(j); 376 if (ignoreClasses.contains(s2)) 377 return "<ignored>"; 378 return s2 + "..." + s3; 379 } 380}