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.wicket.protocol.http; 018 019import static java.lang.System.arraycopy; 020 021import java.time.LocalDateTime; 022import java.time.ZoneId; 023import java.time.format.DateTimeFormatter; 024import java.util.Arrays; 025import java.util.Date; 026import java.util.List; 027import java.util.Map; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.atomic.AtomicInteger; 030import org.apache.wicket.Application; 031import org.apache.wicket.MetaDataKey; 032import org.apache.wicket.Session; 033import org.apache.wicket.request.IRequestHandler; 034import org.apache.wicket.request.cycle.RequestCycle; 035import org.apache.wicket.util.lang.Args; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039/** 040 * Base class that collects request and session information for request logging to enable rich 041 * information about the events that transpired during a single request. Typical HTTPD and/or 042 * Servlet container log files are unusable for determining what happened in the application since 043 * they contain the requested URLs of the form http://example.com/app?wicket:interface:0:0:0, which 044 * doesn't convey any useful information. Requestloggers can show which page was the target of the 045 * request, and which page was rendered as a response, and anything else: resources, Ajax request, 046 * etc. 047 * <p> 048 * The information in the log files can take any format, depending on the request logger 049 * implementation: currently Wicket supports two formats: a {@link RequestLogger legacy, log4j 050 * compatible format}, and a <em>JsonRequestLogger</em> JSON format. 051 */ 052public abstract class AbstractRequestLogger implements IRequestLogger 053{ 054 private static final Logger LOG = LoggerFactory.getLogger(AbstractRequestLogger.class); 055 056 private static final ZoneId ZID = ZoneId.of("GMT"); 057 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS"); 058 059 /** 060 * Key for storing request data in the request cycle's meta data. 061 */ 062 private static MetaDataKey<RequestData> REQUEST_DATA = new MetaDataKey<>() 063 { 064 private static final long serialVersionUID = 1L; 065 }; 066 067 /** 068 * Key for storing session data in the request cycle's meta data. 069 */ 070 private static MetaDataKey<SessionData> SESSION_DATA = new MetaDataKey<>() 071 { 072 private static final long serialVersionUID = 1L; 073 }; 074 075 private final AtomicInteger totalCreatedSessions = new AtomicInteger(); 076 077 private final AtomicInteger peakSessions = new AtomicInteger(); 078 079 private final Map<String, SessionData> liveSessions; 080 081 private final AtomicInteger activeRequests = new AtomicInteger(); 082 083 private final AtomicInteger peakActiveRequests = new AtomicInteger(); 084 085 /** 086 * Rounded request buffer that keeps the request data for the last N requests in the buffer. 087 */ 088 private RequestData[] requestWindow; 089 090 /** 091 * A special object used as a lock before accessing {@linkplain #requestWindow}. 092 * Needed because {@linkplain #requestWindow} is being reassigned in some cases, 093 * e.g. {@link #resizeBuffer()} 094 */ 095 private final Object requestWindowLock = new Object(); 096 097 /** 098 * Cursor pointing to the current writable location in the buffer. Points to the first empty 099 * slot or if the buffer has been filled completely to the oldest request in the buffer. 100 */ 101 private int indexInWindow = 0; 102 103 /** 104 * records the total request time across the sliding request window so that it can be used to 105 * calculate the average request time across the window duration. 106 */ 107 private long totalRequestTime = 0l; 108 109 /** 110 * records the start time of the oldest request across the sliding window so that it can be used 111 * to calculate the average request time across the window duration. 112 */ 113 private Date startTimeOfOldestRequest; 114 115 /** 116 * Construct. 117 */ 118 public AbstractRequestLogger() 119 { 120 int requestsWindowSize = getRequestsWindowSize(); 121 requestWindow = new RequestData[requestsWindowSize]; 122 liveSessions = new ConcurrentHashMap<>(); 123 } 124 125 @Override 126 public int getCurrentActiveRequestCount() 127 { 128 return activeRequests.get(); 129 } 130 131 @Override 132 public int getPeakActiveRequestCount() 133 { 134 return peakActiveRequests.get(); 135 } 136 137 @Override 138 public SessionData[] getLiveSessions() 139 { 140 final SessionData[] sessions = liveSessions.values().toArray(new SessionData[0]); 141 Arrays.sort(sessions); 142 return sessions; 143 } 144 145 @Override 146 public int getPeakSessions() 147 { 148 return peakSessions.get(); 149 } 150 151 @Override 152 public List<RequestData> getRequests() 153 { 154 synchronized (requestWindowLock) 155 { 156 RequestData[] result = new RequestData[hasBufferRolledOver() ? requestWindow.length 157 : indexInWindow]; 158 copyRequestsInOrder(result); 159 return Arrays.asList(result); 160 } 161 } 162 163 /** 164 * Copies all request data into {@code copy} such that the oldest request is in slot 0 and the 165 * most recent request is in slot {@code copy.length} 166 * 167 * @param copy 168 * the target, has to have a capacity of at least {@code requestWindow.length} 169 */ 170 private void copyRequestsInOrder(RequestData[] copy) 171 { 172 int destPos = 0; 173 174 if (hasBufferRolledOver()) 175 { 176 destPos = requestWindow.length - indexInWindow; 177 178 // first copy the oldest requests stored behind the cursor into the copy 179 arraycopy(requestWindow, indexInWindow, copy, 0, destPos); 180 } 181 182 arraycopy(requestWindow, 0, copy, destPos, indexInWindow); 183 } 184 185 /** 186 * @return whether the buffer has been filled to capacity at least once 187 */ 188 private boolean hasBufferRolledOver() 189 { 190 return requestWindow.length > 0 && requestWindow[requestWindow.length - 1] != null; 191 } 192 193 @Override 194 public int getTotalCreatedSessions() 195 { 196 return totalCreatedSessions.get(); 197 } 198 199 @Override 200 public void objectCreated(Object value) 201 { 202 } 203 204 @Override 205 public void objectRemoved(Object value) 206 { 207 } 208 209 @Override 210 public void objectUpdated(Object value) 211 { 212 } 213 214 @Override 215 public void requestTime(long timeTaken) 216 { 217 RequestData requestdata = RequestCycle.get().getMetaData(REQUEST_DATA); 218 if (requestdata != null) 219 { 220 if (activeRequests.get() > 0) 221 { 222 requestdata.setActiveRequest(activeRequests.decrementAndGet()); 223 } 224 Session session = Session.exists() ? Session.get() : null; 225 String sessionId = session != null ? session.getId() : "N/A"; 226 requestdata.setSessionId(sessionId); 227 228 Object sessionInfo = getSessionInfo(session); 229 requestdata.setSessionInfo(sessionInfo); 230 231 long sizeInBytes = -1; 232 if (Application.exists() && 233 Application.get().getRequestLoggerSettings().getRecordSessionSize()) 234 { 235 try 236 { 237 sizeInBytes = session != null ? session.getSizeInBytes() : -1; 238 } 239 catch (Exception e) 240 { 241 // log the error and let the request logging continue (this is what happens in 242 // the detach phase of the request cycle anyway. This provides better 243 // diagnostics). 244 LOG.error( 245 "Exception while determining the size of the session in the request logger: " + 246 e.getMessage(), e); 247 } 248 } 249 requestdata.setSessionSize(sizeInBytes); 250 requestdata.setTimeTaken(timeTaken); 251 252 addRequest(requestdata); 253 254 SessionData sessiondata; 255 if (sessionId != null) 256 { 257 sessiondata = liveSessions.get(sessionId); 258 if (sessiondata == null) 259 { 260 // if the session has been destroyed during the request by 261 // Session#invalidateNow, retrieve the old session data from the RequestCycle. 262 sessiondata = RequestCycle.get().getMetaData(SESSION_DATA); 263 } 264 if (sessiondata == null) 265 { 266 // passivated session or logger only started after it. 267 sessionCreated(sessionId); 268 sessiondata = liveSessions.get(sessionId); 269 } 270 if (sessiondata != null) 271 { 272 sessiondata.setSessionInfo(sessionInfo); 273 sessiondata.setSessionSize(sizeInBytes); 274 sessiondata.addTimeTaken(timeTaken); 275 RequestCycle.get().setMetaData(SESSION_DATA, sessiondata); 276 } 277 } 278 } 279 } 280 281 @Override 282 public void sessionCreated(String sessionId) 283 { 284 liveSessions.put(sessionId, new SessionData(sessionId)); 285 if (liveSessions.size() > peakSessions.get()) 286 { 287 peakSessions.set(liveSessions.size()); 288 } 289 totalCreatedSessions.incrementAndGet(); 290 } 291 292 @Override 293 public void sessionDestroyed(String sessionId) 294 { 295 RequestCycle requestCycle = RequestCycle.get(); 296 SessionData sessionData = liveSessions.remove(sessionId); 297 if (requestCycle != null) 298 requestCycle.setMetaData(SESSION_DATA, sessionData); 299 } 300 301 @Override 302 public RequestData getCurrentRequest() 303 { 304 RequestCycle requestCycle = RequestCycle.get(); 305 RequestData rd = requestCycle.getMetaData(REQUEST_DATA); 306 if (rd == null) 307 { 308 rd = new RequestData(); 309 requestCycle.setMetaData(REQUEST_DATA, rd); 310 int activeCount = activeRequests.incrementAndGet(); 311 312 if (activeCount > peakActiveRequests.get()) 313 { 314 peakActiveRequests.set(activeCount); 315 } 316 } 317 return rd; 318 } 319 320 @Override 321 public void performLogging() 322 { 323 RequestData requestdata = RequestCycle.get().getMetaData(REQUEST_DATA); 324 if (requestdata != null) 325 { 326 // log the request- and sessiondata (the latter can be null) 327 SessionData sessiondata = RequestCycle.get().getMetaData(SESSION_DATA); 328 log(requestdata, sessiondata); 329 } 330 } 331 332 protected abstract void log(RequestData rd, SessionData sd); 333 334 private Object getSessionInfo(Session session) 335 { 336 if (session instanceof ISessionLogInfo) 337 { 338 return ((ISessionLogInfo)session).getSessionInfo(); 339 } 340 return ""; 341 } 342 343 protected void addRequest(RequestData rd) 344 { 345 // ensure the buffer has the proper installed length 346 resizeBuffer(); 347 348 synchronized (requestWindowLock) 349 { 350 // if the requestWindow is a zero-length array, nothing gets stored 351 if (requestWindow.length == 0) 352 return; 353 354 // use the oldest request data to recalculate the average request time 355 RequestData old = requestWindow[indexInWindow]; 356 357 // replace the oldest request with the nweset request 358 requestWindow[indexInWindow] = rd; 359 360 // move the cursor to the next writable position containing the oldest request or if the 361 // buffer hasn't been filled completely the first empty slot 362 indexInWindow = (indexInWindow + 1) % requestWindow.length; 363 if (old != null) 364 { 365 startTimeOfOldestRequest = requestWindow[indexInWindow].getStartDate(); 366 totalRequestTime -= old.getTimeTaken(); 367 } 368 else 369 { 370 if (startTimeOfOldestRequest == null) 371 startTimeOfOldestRequest = rd.getStartDate(); 372 } 373 totalRequestTime += rd.getTimeTaken(); 374 } 375 } 376 377 private int getWindowSize() 378 { 379 if (requestWindow[requestWindow.length - 1] == null) 380 return indexInWindow; 381 else 382 return requestWindow.length; 383 } 384 385 @Override 386 public long getAverageRequestTime() 387 { 388 synchronized (requestWindowLock) 389 { 390 int windowSize = getWindowSize(); 391 if (windowSize == 0) 392 return 0; 393 return totalRequestTime / windowSize; 394 } 395 } 396 397 @Override 398 public long getRequestsPerMinute() 399 { 400 synchronized (requestWindowLock) 401 { 402 int windowSize = getWindowSize(); 403 if (windowSize == 0) 404 return 0; 405 long start = startTimeOfOldestRequest.getTime(); 406 long end = System.currentTimeMillis(); 407 double diff = end - start; 408 return Math.round(windowSize / (diff / 60000.0)); 409 } 410 } 411 412 @Override 413 public void logEventTarget(IRequestHandler requestHandler) 414 { 415 RequestData requestData = getCurrentRequest(); 416 if (requestData != null) 417 { 418 requestData.setEventTarget(requestHandler); 419 } 420 } 421 422 @Override 423 public void logRequestedUrl(String url) 424 { 425 getCurrentRequest().setRequestedUrl(url); 426 } 427 428 @Override 429 public void logResponseTarget(IRequestHandler requestHandler) 430 { 431 RequestData requestData = getCurrentRequest(); 432 if (requestData != null) 433 requestData.setResponseTarget(requestHandler); 434 } 435 436 /** 437 * Resizes the request buffer to match the 438 * {@link org.apache.wicket.settings.RequestLoggerSettings#getRequestsWindowSize() configured window size} 439 */ 440 private void resizeBuffer() 441 { 442 int newCapacity = getRequestsWindowSize(); 443 444 // do nothing if the capacity requirement hasn't changed 445 if (newCapacity == requestWindow.length) 446 return; 447 448 RequestData[] newRequestWindow = new RequestData[newCapacity]; 449 synchronized (requestWindowLock) 450 { 451 int oldCapacity = requestWindow.length; 452 int oldNumberOfElements = hasBufferRolledOver() ? oldCapacity : indexInWindow; 453 454 if (newCapacity > oldCapacity) 455 { 456 // increase the capacity of the buffer when more requests need to be stored 457 // and preserve the order of the requests while copying them into the new buffer. 458 copyRequestsInOrder(newRequestWindow); 459 460 // the next writable position is at the first non-copied element in the buffer 461 indexInWindow = oldNumberOfElements; 462 requestWindow = newRequestWindow; 463 } 464 else if (newCapacity < oldCapacity) 465 { 466 // sort the requests in the current buffer such that the oldest request is in slot 0 467 RequestData[] sortedRequestWindow = new RequestData[oldCapacity]; 468 copyRequestsInOrder(sortedRequestWindow); 469 470 // determine the number of elements that need to be copied into the smaller target 471 int numberOfElementsToCopy = Math.min(newCapacity, oldNumberOfElements); 472 473 // determine the position from where the copying must start 474 int numberOfElementsToSkip = Math.max(0, oldNumberOfElements - 475 numberOfElementsToCopy); 476 477 // fill the new buffer with the leftovers of the old buffer, skipping the oldest 478 // requests 479 arraycopy(sortedRequestWindow, numberOfElementsToSkip, newRequestWindow, 0, 480 numberOfElementsToCopy); 481 482 // the next writable position is 0 when the buffer is filled to capacity, or the 483 // number of copied elements when the buffer isn't filled to capacity. 484 indexInWindow = numberOfElementsToCopy >= newCapacity ? 0 : numberOfElementsToCopy; 485 requestWindow = newRequestWindow; 486 } 487 } 488 } 489 490 491 /** 492 * Thread-safely formats the passed date in format 'yyyy-MM-dd hh:mm:ss,SSS' with GMT timezone 493 * 494 * @param date 495 * the date to format 496 * @return the formatted date 497 */ 498 protected String formatDate(final Date date) 499 { 500 Args.notNull(date, "date"); 501 502 LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZID); 503 return ldt.format(FORMATTER); 504 } 505 506 private int getRequestsWindowSize() 507 { 508 int requestsWindowSize = 0; 509 if (Application.exists()) 510 { 511 requestsWindowSize = Application.get() 512 .getRequestLoggerSettings() 513 .getRequestsWindowSize(); 514 } 515 return requestsWindowSize; 516 } 517}