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}