001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.internal.services.exceptions; 014 015import org.apache.commons.io.IOUtils; 016import org.apache.tapestry5.SymbolConstants; 017import org.apache.tapestry5.func.F; 018import org.apache.tapestry5.func.Flow; 019import org.apache.tapestry5.func.Mapper; 020import org.apache.tapestry5.func.Reducer; 021import org.apache.tapestry5.internal.TapestryInternalUtils; 022import org.apache.tapestry5.ioc.annotations.Inject; 023import org.apache.tapestry5.ioc.annotations.Symbol; 024import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.ioc.services.ExceptionAnalysis; 027import org.apache.tapestry5.ioc.services.ExceptionAnalyzer; 028import org.apache.tapestry5.ioc.services.ExceptionInfo; 029import org.apache.tapestry5.ioc.util.ExceptionUtils; 030import org.apache.tapestry5.services.ExceptionReporter; 031import org.apache.tapestry5.services.Request; 032import org.apache.tapestry5.services.RequestGlobals; 033import org.slf4j.Logger; 034 035import java.io.File; 036import java.io.IOException; 037import java.io.PrintWriter; 038import java.lang.reflect.Array; 039import java.util.*; 040import java.util.concurrent.atomic.AtomicInteger; 041 042@SuppressWarnings("ResultOfMethodCallIgnored") 043public class ExceptionReporterImpl implements ExceptionReporter 044{ 045 private static final Reducer<Integer, Integer> MAX = new Reducer<Integer, Integer>() 046 { 047 @Override 048 public Integer reduce(Integer accumulator, Integer element) 049 { 050 return Math.max(accumulator, element); 051 } 052 }; 053 054 private static final Mapper<String, Integer> STRING_TO_LENGTH = new Mapper<String, Integer>() 055 { 056 @Override 057 public Integer map(String element) 058 { 059 return element.length(); 060 } 061 }; 062 063 @Inject 064 @Symbol(SymbolConstants.EXCEPTION_REPORTS_DIR) 065 private File logDir; 066 067 @Inject 068 @Symbol(SymbolConstants.CONTEXT_PATH) 069 private String contextPath; 070 071 @Inject 072 @Symbol(SymbolConstants.RESTRICTIVE_ENVIRONMENT) 073 private boolean restrictive; 074 075 @Inject 076 private ExceptionAnalyzer analyzer; 077 078 private final AtomicInteger uid = new AtomicInteger(); 079 080 @Inject 081 private Logger logger; 082 083 @Inject 084 private RequestGlobals requestGlobals; 085 086 @Override 087 public void reportException(Throwable exception) 088 { 089 Date date = new Date(); 090 String fileName = String.format( 091 "exception-%tY%<tm%<td-%<tH%<tM%<tS-%<tL.%d.txt", date, 092 uid.getAndIncrement()); 093 094 File folder; 095 096 try 097 { 098 if (restrictive) 099 { 100 // Good luck with this; all exceptions written to a single folder. 101 folder = logDir; 102 } else 103 { 104 String folderName = String.format("%tY-%<tm-%<td/%<tH/%<tM", date); 105 folder = new File(logDir, folderName); 106 107 folder.mkdirs(); 108 } 109 110 File log = new File(folder, fileName); 111 112 writeExceptionToFile(exception, log); 113 114 logger.warn(String.format("Wrote exception report to %s", toURI(log))); 115 } catch (Exception ex) 116 { 117 logger.error(String.format("Unable to write exception report %s: %s", 118 fileName, ExceptionUtils.toMessage(ex))); 119 120 logger.error("Original exception:", exception); 121 } 122 } 123 124 private String toURI(File file) 125 { 126 try 127 { 128 return file.toURI().toString(); 129 } catch (Exception e) 130 { 131 return file.toString(); 132 } 133 } 134 135 private void writeExceptionToFile(Throwable exception, File log) throws IOException 136 { 137 log.createNewFile(); 138 ExceptionAnalysis analysis = analyzer.analyze(exception); 139 PrintWriter writer = null; 140 try 141 { 142 writer = new PrintWriter(log); 143 writeException(writer, analysis); 144 } finally 145 { 146 IOUtils.closeQuietly(writer); 147 } 148 } 149 150 interface PropertyWriter 151 { 152 void write(String name, Object value); 153 } 154 155 private final static Mapper<ExceptionInfo, Flow<String>> EXCEPTION_INFO_TO_PROPERTY_NAMES = 156 new Mapper<ExceptionInfo, Flow<String>>() 157 { 158 @Override 159 public Flow<String> map(ExceptionInfo element) 160 { 161 return F.flow(element.getPropertyNames()); 162 } 163 }; 164 165 private void writeException(final PrintWriter writer, ExceptionAnalysis analysis) 166 { 167 final Formatter f = new Formatter(writer); 168 writer.print("EXCEPTION STACK:\n\n"); 169 Request request = requestGlobals.getRequest(); 170 171 // Figure out what all the property names are so that we can set the width of the column that lists 172 // property names. 173 Flow<String> propertyNames = F.flow(analysis.getExceptionInfos()) 174 .mapcat(EXCEPTION_INFO_TO_PROPERTY_NAMES).append("Exception type", "Message"); 175 176 if (request != null) 177 { 178 propertyNames = propertyNames.concat(request.getParameterNames()).concat(request.getHeaderNames()); 179 } 180 181 final int maxPropertyNameLength = propertyNames.map(STRING_TO_LENGTH).reduce(MAX, 0); 182 183 final String propertyNameFormat = " %" + maxPropertyNameLength + "s: %s\n"; 184 185 PropertyWriter pw = new PropertyWriter() 186 { 187 @SuppressWarnings("rawtypes") 188 @Override 189 public void write(String name, Object value) 190 { 191 if (value.getClass().isArray()) 192 { 193 write(name, toList(value)); 194 return; 195 } 196 197 if (value instanceof Iterable) 198 { 199 boolean first = true; 200 Iterable iterable = (Iterable) value; 201 Iterator i = iterable.iterator(); 202 while (i.hasNext()) 203 { 204 if (first) 205 { 206 f.format(propertyNameFormat, name, i.next()); 207 first = false; 208 } else 209 { 210 for (int j = 0; j < maxPropertyNameLength + 4; j++) 211 writer.write(' '); 212 213 writer.println(i.next()); 214 } 215 } 216 return; 217 } 218 219 // TODO: Handling of arrays & collections 220 f.format(propertyNameFormat, name, value); 221 } 222 223 @SuppressWarnings({"rawtypes", "unchecked"}) 224 private List toList(Object array) 225 { 226 int count = Array.getLength(array); 227 List result = new ArrayList(count); 228 for (int i = 0; i < count; i++) 229 { 230 result.add(Array.get(array, i)); 231 } 232 return result; 233 } 234 }; 235 236 boolean first = true; 237 238 for (ExceptionInfo info : analysis.getExceptionInfos()) 239 { 240 if (first) 241 { 242 writer.println(); 243 first = false; 244 } 245 pw.write("Exception type", info.getClassName()); 246 pw.write("Message", info.getMessage()); 247 for (String name : info.getPropertyNames()) 248 { 249 pw.write(name, info.getProperty(name)); 250 } 251 if (!info.getStackTrace().isEmpty()) 252 { 253 writer.write("\n Stack trace:\n"); 254 for (StackTraceElement e : info.getStackTrace()) 255 { 256 f.format(" - %s\n", e.toString()); 257 } 258 } 259 writer.println(); 260 } 261 262 if (request != null) 263 { 264 writer.print("REQUEST:\n\nBasic Information:\n"); 265 List<String> flags = CollectionFactory.newList(); 266 if (request.isXHR()) 267 { 268 flags.add("XHR"); 269 } 270 if (request.isRequestedSessionIdValid()) 271 { 272 flags.add("requestedSessionIdValid"); 273 } 274 if (request.isSecure()) 275 { 276 flags.add("secure"); 277 } 278 pw.write("contextPath", contextPath); 279 if (!flags.isEmpty()) 280 { 281 pw.write("flags", InternalUtils.joinSorted(flags)); 282 } 283 pw.write("method", request.getMethod()); 284 pw.write("path", request.getPath()); 285 pw.write("locale", request.getLocale()); 286 pw.write("serverName", request.getServerName()); 287 pw.write("remoteHost", request.getRemoteHost()); 288 writer.print("\nHeaders:\n"); 289 for (String name : request.getHeaderNames()) 290 { 291 pw.write(name, request.getHeader(name)); 292 } 293 if (!request.getParameterNames().isEmpty()) 294 { 295 writer.print("\nParameters:\n"); 296 for (String name : request.getParameterNames()) 297 { 298 // TODO: Support multi-value parameters 299 pw.write(name, request.getParameters(name)); 300 } 301 } 302 // TODO: Session if it exists 303 } 304 305 writer.print("\nSYSTEM INFORMATION:"); 306 307 Runtime runtime = Runtime.getRuntime(); 308 309 f.format("\n\nMemory:\n %,15d bytes free\n %,15d bytes total\n %,15d bytes max\n", 310 runtime.freeMemory(), 311 runtime.totalMemory(), 312 runtime.maxMemory()); 313 314 Thread[] threads = TapestryInternalUtils.getAllThreads(); 315 316 int maxThreadNameLength = 0; 317 318 for (Thread t : threads) 319 { 320 maxThreadNameLength = Math.max(maxThreadNameLength, t.getName().length()); 321 } 322 323 String format = "\n%s %" + maxThreadNameLength + "s %s"; 324 325 f.format("\n%,d Threads:", threads.length); 326 327 for (Thread t : threads) 328 { 329 f.format(format, 330 Thread.currentThread() == t ? "*" : " ", 331 t.getName(), 332 t.getState().name()); 333 if (t.isDaemon()) 334 { 335 writer.write(", daemon"); 336 } 337 if (!t.isAlive()) 338 { 339 writer.write(", NOT alive"); 340 } 341 if (t.isInterrupted()) 342 { 343 writer.write(", interrupted"); 344 } 345 if (t.getPriority() != Thread.NORM_PRIORITY) 346 { 347 f.format(", priority %d", t.getPriority()); 348 } 349 } 350 writer.println(); 351 352 f.close(); 353 } 354 355}