| /******************************************************************************* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| *******************************************************************************/ |
| package org.apache.ofbiz.webapp.stats; |
| |
| import java.util.Date; |
| import java.util.Deque; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentLinkedDeque; |
| import java.util.concurrent.ConcurrentMap; |
| |
| import javax.servlet.http.HttpServletRequest; |
| |
| import org.apache.ofbiz.base.util.Debug; |
| import org.apache.ofbiz.base.util.UtilHttp; |
| import org.apache.ofbiz.base.util.UtilProperties; |
| import org.apache.ofbiz.base.util.UtilValidate; |
| import org.apache.ofbiz.entity.Delegator; |
| import org.apache.ofbiz.entity.DelegatorFactory; |
| import org.apache.ofbiz.entity.GenericEntityException; |
| import org.apache.ofbiz.entity.GenericValue; |
| import org.apache.ofbiz.entity.model.ModelEntity; |
| import org.apache.ofbiz.entity.util.EntityQuery; |
| import org.apache.ofbiz.entity.util.EntityUtilProperties; |
| |
| import com.ibm.icu.util.Calendar; |
| |
| /** |
| * <p>Counts server hits and tracks statistics for request, events and views |
| * <p>Handles total stats since the server started and binned |
| * stats according to settings in the serverstats.properties file. |
| */ |
| public class ServerHitBin { |
| // Debug module name |
| public static final String module = ServerHitBin.class.getName(); |
| |
| public static final int REQUEST = 1; |
| public static final int EVENT = 2; |
| public static final int VIEW = 3; |
| public static final int ENTITY = 4; |
| public static final int SERVICE = 5; |
| |
| private static final String[] typeIds = {"", "REQUEST", "EVENT", "VIEW", "ENTITY", "SERVICE"}; |
| |
| // these Maps contain Lists of ServerHitBin objects by id, the most recent is first in the list |
| public static final ConcurrentMap<String, Deque<ServerHitBin>> requestHistory = new ConcurrentHashMap<String, Deque<ServerHitBin>>(); |
| public static final ConcurrentMap<String, Deque<ServerHitBin>> eventHistory = new ConcurrentHashMap<String, Deque<ServerHitBin>>(); |
| public static final ConcurrentMap<String, Deque<ServerHitBin>> viewHistory = new ConcurrentHashMap<String, Deque<ServerHitBin>>(); |
| public static final ConcurrentMap<String, Deque<ServerHitBin>> entityHistory = new ConcurrentHashMap<String, Deque<ServerHitBin>>(); |
| public static final ConcurrentMap<String, Deque<ServerHitBin>> serviceHistory = new ConcurrentHashMap<String, Deque<ServerHitBin>>(); |
| |
| // these Maps contain ServerHitBin objects by id |
| public static final ConcurrentMap<String, ServerHitBin> requestSinceStarted = new ConcurrentHashMap<String, ServerHitBin>(); |
| public static final ConcurrentMap<String, ServerHitBin> eventSinceStarted = new ConcurrentHashMap<String, ServerHitBin>(); |
| public static final ConcurrentMap<String, ServerHitBin> viewSinceStarted = new ConcurrentHashMap<String, ServerHitBin>(); |
| public static final ConcurrentMap<String, ServerHitBin> entitySinceStarted = new ConcurrentHashMap<String, ServerHitBin>(); |
| public static final ConcurrentMap<String, ServerHitBin> serviceSinceStarted = new ConcurrentHashMap<String, ServerHitBin>(); |
| |
| public static void countRequest(String id, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| countHit(id, REQUEST, request, startTime, runningTime, userLogin); |
| } |
| |
| public static void countEvent(String id, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| countHit(id, EVENT, request, startTime, runningTime, userLogin); |
| } |
| |
| public static void countView(String id, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| countHit(id, VIEW, request, startTime, runningTime, userLogin); |
| } |
| |
| public static void countEntity(String id, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| countHit(id, ENTITY, request, startTime, runningTime, userLogin); |
| } |
| |
| public static void countService(String id, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| countHit(id, SERVICE, request, startTime, runningTime, userLogin); |
| } |
| |
| private static void countHit(String id, int type, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) { |
| // only count hits if enabled, if not specified defaults to false |
| if (!"true".equals(UtilProperties.getPropertyValue("serverstats", "stats.enable." + typeIds[type]))) return; |
| countHit(id, type, request, startTime, runningTime, userLogin, true); |
| } |
| |
| private static String makeIdTenantAware(String id, Delegator delegator) { |
| if (UtilValidate.isNotEmpty(delegator.getDelegatorTenantId())) { |
| return id + "#" + delegator.getDelegatorTenantId(); |
| } else { |
| return id; |
| } |
| } |
| |
| private static long getNewBinLength() { |
| long binLength = (long) UtilProperties.getPropertyNumber("serverstats", "stats.bin.length.millis"); |
| |
| // if no or 0 binLength specified, set to 30 minutes |
| if (binLength <= 0) binLength = 1800000; |
| // if binLength is more than an hour, set it to one hour |
| if (binLength > 3600000) binLength = 3600000; |
| return binLength; |
| } |
| |
| private static long getEvenStartingTime(long binLength) { |
| // binLengths should be a divisable evenly into 1 hour |
| long curTime = System.currentTimeMillis(); |
| |
| // find the first previous millis that are even on the hour |
| Calendar cal = Calendar.getInstance(); |
| |
| cal.setTime(new Date(curTime)); |
| cal.set(Calendar.MINUTE, 0); |
| cal.set(Calendar.SECOND, 0); |
| cal.set(Calendar.MILLISECOND, 0); |
| |
| while (cal.getTime().getTime() < (curTime - binLength)) { |
| cal.add(Calendar.MILLISECOND, (int) binLength); |
| } |
| |
| return cal.getTime().getTime(); |
| } |
| |
| private static void countHit(String baseId, int type, HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin, boolean isOriginal) { |
| Delegator delegator = (Delegator) request.getAttribute("delegator"); |
| if (delegator == null) { |
| String delegatorName = (String) request.getSession().getAttribute("delegatorName"); |
| delegator = DelegatorFactory.getDelegator(delegatorName); |
| } |
| if (delegator == null) { |
| throw new IllegalArgumentException("In countHit could not find a delegator or delegatorName to work from"); |
| } |
| |
| String id = makeIdTenantAware(baseId, delegator); |
| |
| ServerHitBin bin = null; |
| Deque<ServerHitBin> binList = null; |
| |
| switch (type) { |
| case REQUEST: |
| binList = requestHistory.get(id); |
| break; |
| |
| case EVENT: |
| binList = eventHistory.get(id); |
| break; |
| |
| case VIEW: |
| binList = viewHistory.get(id); |
| break; |
| |
| case ENTITY: |
| binList = entityHistory.get(id); |
| break; |
| |
| case SERVICE: |
| binList = serviceHistory.get(id); |
| break; |
| } |
| |
| if (binList == null) { |
| binList = new ConcurrentLinkedDeque<ServerHitBin>(); |
| Deque<ServerHitBin> listFromMap = null; |
| switch (type) { |
| case REQUEST: |
| listFromMap = requestHistory.putIfAbsent(id, binList); |
| break; |
| |
| case EVENT: |
| listFromMap = eventHistory.putIfAbsent(id, binList); |
| break; |
| |
| case VIEW: |
| listFromMap = viewHistory.putIfAbsent(id, binList); |
| break; |
| |
| case ENTITY: |
| listFromMap = entityHistory.putIfAbsent(id, binList); |
| break; |
| |
| case SERVICE: |
| listFromMap = serviceHistory.putIfAbsent(id, binList); |
| break; |
| } |
| binList = listFromMap != null ? listFromMap : binList; |
| } |
| |
| do { |
| bin = binList.peek(); |
| if (bin == null) { |
| binList.addFirst(new ServerHitBin(id, type, true, delegator)); |
| } |
| } while (bin == null); |
| |
| long toTime = startTime + runningTime; |
| // advance the bin |
| // first check to see if the bin has expired, if so save and recycle it |
| while (bin.limitLength && toTime > bin.endTime) { |
| // the first in the list will be this object, remove and copy it, |
| // put the copy at the first of the list, then put this object back on |
| if (bin.getNumberHits() > 0) { |
| // persist each bin when time ends if option turned on |
| if (EntityUtilProperties.propertyValueEqualsIgnoreCase("serverstats", "stats.persist." + ServerHitBin.typeIds[type] + ".bin", "true", delegator)) { |
| GenericValue serverHitBin = delegator.makeValue("ServerHitBin"); |
| serverHitBin.set("contentId", bin.id); |
| serverHitBin.set("hitTypeId", ServerHitBin.typeIds[bin.type]); |
| serverHitBin.set("binStartDateTime", new java.sql.Timestamp(bin.startTime)); |
| serverHitBin.set("binEndDateTime", new java.sql.Timestamp(bin.endTime)); |
| serverHitBin.set("numberHits", Long.valueOf(bin.getNumberHits())); |
| serverHitBin.set("totalTimeMillis", Long.valueOf(bin.getTotalRunningTime())); |
| serverHitBin.set("minTimeMillis", Long.valueOf(bin.getMinTime())); |
| serverHitBin.set("maxTimeMillis", Long.valueOf(bin.getMaxTime())); |
| // get localhost ip address and hostname to store |
| if (VisitHandler.address != null) { |
| serverHitBin.set("serverIpAddress", VisitHandler.address.getHostAddress()); |
| serverHitBin.set("serverHostName", VisitHandler.address.getHostName()); |
| } |
| try { |
| delegator.createSetNextSeqId(serverHitBin); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, "Could not save ServerHitBin:", module); |
| } |
| } |
| } else { |
| binList.pollFirst(); |
| } |
| bin = new ServerHitBin(bin, bin.endTime + 1); |
| binList.addFirst(bin); |
| } |
| |
| bin.addHit(runningTime); |
| if (isOriginal) { |
| try { |
| bin.saveHit(request, startTime, runningTime, userLogin); |
| } catch (GenericEntityException e) { |
| Debug.logWarning("Error saving ServerHit: " + e.toString(), module); |
| } |
| } |
| |
| // count since start global and per id hits |
| if (!id.startsWith("GLOBAL")) { |
| countHitSinceStart(id, type, runningTime, delegator); |
| if (isOriginal) { |
| countHitSinceStart(makeIdTenantAware("GLOBAL", delegator), type, runningTime, delegator); |
| } |
| } |
| |
| // also count hits up the hierarchy if the id contains a '.' |
| if (id.indexOf('.') > 0) { |
| countHit(id.substring(0, id.lastIndexOf('.')), type, request, startTime, runningTime, userLogin, false); |
| } |
| |
| if (isOriginal) { |
| countHit("GLOBAL", type, request, startTime, runningTime, userLogin, false); |
| } |
| } |
| |
| private static void countHitSinceStart(String id, int type, long runningTime, Delegator delegator) { |
| ServerHitBin bin = null; |
| |
| switch (type) { |
| case REQUEST: |
| bin = requestSinceStarted.get(id); |
| break; |
| |
| case EVENT: |
| bin = eventSinceStarted.get(id); |
| break; |
| |
| case VIEW: |
| bin = viewSinceStarted.get(id); |
| break; |
| |
| case ENTITY: |
| bin = entitySinceStarted.get(id); |
| break; |
| |
| case SERVICE: |
| bin = serviceSinceStarted.get(id); |
| break; |
| } |
| |
| if (bin == null) { |
| bin = new ServerHitBin(id, type, false, delegator); |
| ServerHitBin binFromMap = null; |
| switch (type) { |
| case REQUEST: |
| binFromMap = requestSinceStarted.putIfAbsent(id, bin); |
| break; |
| |
| case EVENT: |
| binFromMap = eventSinceStarted.putIfAbsent(id, bin); |
| break; |
| |
| case VIEW: |
| binFromMap = viewSinceStarted.putIfAbsent(id, bin); |
| break; |
| |
| case ENTITY: |
| binFromMap = entitySinceStarted.putIfAbsent(id, bin); |
| break; |
| |
| case SERVICE: |
| binFromMap = serviceSinceStarted.putIfAbsent(id, bin); |
| break; |
| } |
| bin = binFromMap != null ? binFromMap : bin; |
| } |
| bin.addHit(runningTime); |
| } |
| |
| private final Delegator delegator; |
| private final String id; |
| private final int type; |
| private final boolean limitLength; |
| private final long binLength; |
| private final long startTime; |
| private final long endTime; |
| |
| private long numberHits; |
| private long totalRunningTime; |
| private long minTime; |
| private long maxTime; |
| |
| private ServerHitBin(String id, int type, boolean limitLength, Delegator delegator) { |
| this.id = id; |
| this.type = type; |
| this.limitLength = limitLength; |
| this.delegator = delegator; |
| this.binLength = getNewBinLength(); |
| this.startTime = getEvenStartingTime(this.binLength); |
| if (this.limitLength) { |
| // subtract 1 millisecond to keep bin starting times even |
| this.endTime = this.startTime + this.binLength - 1; |
| } else { |
| this.endTime = 0; |
| } |
| this.numberHits = 0; |
| this.totalRunningTime = 0; |
| this.minTime = Long.MAX_VALUE; |
| this.maxTime = 0; |
| } |
| |
| private ServerHitBin(ServerHitBin oldBin, long startTime) { |
| this.id = oldBin.id; |
| this.type = oldBin.type; |
| this.limitLength = oldBin.limitLength; |
| this.delegator = oldBin.delegator; |
| this.binLength = oldBin.binLength; |
| |
| this.startTime = startTime; |
| if (limitLength) { |
| // subtract 1 millisecond to keep bin starting times even |
| this.endTime = this.startTime + this.binLength - 1; |
| } else { |
| this.endTime = 0; |
| } |
| this.numberHits = 0; |
| this.totalRunningTime = 0; |
| this.minTime = Long.MAX_VALUE; |
| this.maxTime = 0; |
| } |
| |
| public Delegator getDelegator() { |
| return this.delegator; |
| } |
| |
| public String getId() { |
| return this.id; |
| } |
| |
| public int getType() { |
| return this.type; |
| } |
| |
| /** returns the startTime of the bin */ |
| public long getStartTime() { |
| return this.startTime; |
| } |
| |
| /** Returns the end time if the length of the bin is limited, otherwise returns the current system time */ |
| public long getEndTime() { |
| return limitLength ? this.endTime : System.currentTimeMillis(); |
| } |
| |
| /** returns the startTime of the bin */ |
| public String getStartTimeString() { |
| // using Timestamp toString because I like the way it formats it |
| return new java.sql.Timestamp(this.getStartTime()).toString(); |
| } |
| |
| /** Returns the end time if the length of the bin is limited, otherwise returns the current system time */ |
| public String getEndTimeString() { |
| return new java.sql.Timestamp(this.getEndTime()).toString(); |
| } |
| |
| /** returns endTime - startTime */ |
| public long getBinLength() { |
| return this.getEndTime() - this.getStartTime(); |
| } |
| |
| /** returns (endTime - startTime)/60000 */ |
| public double getBinLengthMinutes() { |
| return (this.getBinLength()) / 60000.0; |
| } |
| |
| public synchronized long getNumberHits() { |
| return this.numberHits; |
| } |
| |
| public synchronized long getMinTime() { |
| return this.minTime; |
| } |
| |
| public synchronized long getMaxTime() { |
| return this.maxTime; |
| } |
| |
| public synchronized long getTotalRunningTime() { |
| return this.totalRunningTime; |
| } |
| |
| public double getMinTimeSeconds() { |
| return (this.getMinTime()) / 1000.0; |
| } |
| |
| public double getMaxTimeSeconds() { |
| return (this.getMaxTime()) / 1000.0; |
| } |
| |
| public synchronized double getAvgTime() { |
| return ((double) this.getTotalRunningTime()) / ((double) this.getNumberHits()); |
| } |
| |
| public double getAvgTimeSeconds() { |
| return this.getAvgTime() / 1000.0; |
| } |
| |
| /** return the hits per minute using the entire length of the bin as returned by getBinLengthMinutes() */ |
| public double getHitsPerMinute() { |
| return this.getNumberHits() / this.getBinLengthMinutes(); |
| } |
| |
| private synchronized void addHit(long runningTime) { |
| this.numberHits++; |
| this.totalRunningTime += runningTime; |
| if (runningTime < this.minTime) |
| this.minTime = runningTime; |
| if (runningTime > this.maxTime) |
| this.maxTime = runningTime; |
| } |
| |
| private void saveHit(HttpServletRequest request, long startTime, long runningTime, GenericValue userLogin) throws GenericEntityException { |
| // persist record of hit in ServerHit entity if option turned on |
| Delegator delegator = (Delegator) request.getAttribute("delegator"); |
| if (EntityUtilProperties.propertyValueEqualsIgnoreCase("serverstats", "stats.persist." + ServerHitBin.typeIds[type] + ".hit", "true", delegator)) { |
| // if the hit type is ENTITY and the name contains "ServerHit" don't |
| // persist; avoids the infinite loop and a bunch of annoying data |
| if (this.type == ENTITY && this.id.indexOf("ServerHit") > 0) { |
| return; |
| } |
| |
| // check for type data before running. |
| GenericValue serverHitType = null; |
| |
| serverHitType = EntityQuery.use(delegator).from("ServerHitType").where("hitTypeId", ServerHitBin.typeIds[this.type]).cache().queryOne(); |
| if (serverHitType == null) { |
| // datamodel data not loaded; not storing hit. |
| Debug.logWarning("The datamodel data has not been loaded; cannot find hitTypeId '" + ServerHitBin.typeIds[this.type] + " not storing ServerHit.", module); |
| return; |
| } |
| |
| GenericValue visit = VisitHandler.getVisit(request.getSession()); |
| if (visit == null) { |
| // no visit info stored, so don't store the ServerHit |
| Debug.logWarning("Could not find a visitId, so not storing ServerHit. This is probably a configuration error. If you turn off persistance of visits you should also turn off persistence of hits.", module); |
| return; |
| } |
| String visitId = visit.getString("visitId"); |
| visit = EntityQuery.use(delegator).from("Visit").where("visitId", visitId).queryOne(); |
| if (visit == null) { |
| // GenericValue stored in client session does not exist in database. |
| Debug.logInfo("The Visit GenericValue stored in the client session does not exist in the database, not storing server hit.", module); |
| return; |
| } |
| |
| Debug.logInfo("Visit delegatorName=" + visit.getDelegator().getDelegatorName() + ", ServerHitBin delegatorName=" + this.delegator.getDelegatorName(), module); |
| |
| GenericValue serverHit = delegator.makeValue("ServerHit"); |
| |
| serverHit.set("visitId", visitId); |
| serverHit.set("hitStartDateTime", new java.sql.Timestamp(startTime)); |
| serverHit.set("hitTypeId", ServerHitBin.typeIds[this.type]); |
| if (userLogin != null) { |
| serverHit.set("userLoginId", userLogin.get("userLoginId")); |
| ModelEntity modelUserLogin = userLogin.getModelEntity(); |
| if (modelUserLogin.isField("partyId")) { |
| serverHit.set("partyId", userLogin.get("partyId")); |
| } |
| } |
| serverHit.set("contentId", this.id); |
| serverHit.set("runningTimeMillis", Long.valueOf(runningTime)); |
| |
| String fullRequestUrl = UtilHttp.getFullRequestUrl(request); |
| |
| serverHit.set("requestUrl", fullRequestUrl.length() > 250 ? fullRequestUrl.substring(0, 250) : fullRequestUrl); |
| String referrerUrl = request.getHeader("Referer") != null ? request.getHeader("Referer") : ""; |
| |
| serverHit.set("referrerUrl", referrerUrl.length() > 250 ? referrerUrl.substring(0, 250) : referrerUrl); |
| |
| // get localhost ip address and hostname to store |
| if (VisitHandler.address != null) { |
| serverHit.set("serverIpAddress", VisitHandler.address.getHostAddress()); |
| serverHit.set("serverHostName", VisitHandler.address.getHostName()); |
| } |
| |
| // The problem with |
| // |
| // serverHit.create(); |
| // |
| // is that if there are two requests with the same startTime (this should only happen with MySQL see https://issues.apache.org/jira/browse/OFBIZ-2208) |
| // then this will go wrong and abort the actual |
| // transaction we are interested in. |
| // Another way instead of using create is to store or update, |
| // that is overwrite in case there already was an entry, thus |
| // avoiding the transaction being aborted which is not |
| // less desirable than having multiple requests with the |
| // same startTime overwriting each other. |
| // This may not satisfy those who want to record each and |
| // every server hit even with equal startTimes but that could be |
| // solved adding a counter to the ServerHit's PK (a counter |
| // counting multiple hits at the same startTime). |
| serverHit.create(); |
| } |
| } |
| } |