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