com.dynatrace.openkit.core.objects.SessionProxyImpl Maven / Gradle / Ivy
Show all versions of openkit-java Show documentation
/**
* Copyright 2018-2021 Dynatrace LLC
*
* Licensed 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 com.dynatrace.openkit.core.objects;
import com.dynatrace.openkit.api.ConnectionType;
import com.dynatrace.openkit.api.Logger;
import com.dynatrace.openkit.api.RootAction;
import com.dynatrace.openkit.api.Session;
import com.dynatrace.openkit.api.WebRequestTracer;
import com.dynatrace.openkit.core.BeaconSender;
import com.dynatrace.openkit.core.SessionWatchdog;
import com.dynatrace.openkit.core.configuration.ServerConfiguration;
import com.dynatrace.openkit.core.configuration.ServerConfigurationUpdateCallback;
import com.dynatrace.openkit.protocol.Beacon;
import com.dynatrace.openkit.providers.TimingProvider;
import com.dynatrace.openkit.util.json.objects.JSONValue;
import java.io.IOException;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implements a surrogate for a {@link Session} to perform session splitting after:
*
* - a configured number of events
* - after a configured idle timeout
* - after a configured maximum session duration
*
*/
public class SessionProxyImpl extends OpenKitComposite implements Session, ServerConfigurationUpdateCallback {
// object used for synchronization.
private final Object lockObject = new Object();
// log message reporter
private final Logger logger;
// Parent object of this session proxy
private final OpenKitComposite parent;
// creator for split sessions
private final SessionCreator sessionCreator;
// provider to obtain the current time
private final TimingProvider timingProvider;
// sender of beacon data
private final BeaconSender beaconSender;
// watchdog to split sessions after idle/max timeout or to close split off sessions which were not closable on split
private final SessionWatchdog sessionWatchdog;
// the current session instance
private SessionImpl currentSession;
// holds the number of received calls to enterAction
private int topLevelActionCount = 0;
// specifies the timestamp when the last top level event happened
private long lastInteractionTime;
// the server configuration of the first session (will be initialized when first session is updated with server config)
private ServerConfiguration serverConfiguration;
// indicates if this session proxy was already finished
private boolean isFinished;
// last user tag reported via identifyUser
private String lastUserTag = null;
SessionProxyImpl(
Logger logger,
OpenKitComposite parent,
SessionCreator sessionCreator,
TimingProvider timingProvider,
BeaconSender beaconSender,
SessionWatchdog sessionWatchdog
) {
this.logger = logger;
this.parent = parent;
this.sessionCreator = sessionCreator;
this.timingProvider = timingProvider;
this.beaconSender = beaconSender;
this.sessionWatchdog = sessionWatchdog;
ServerConfiguration currentServerConfig = beaconSender.getLastServerConfiguration();
createInitialSessionAndMakeCurrent(currentServerConfig);
}
@Override
public RootAction enterAction(String actionName) {
if (actionName == null || actionName.isEmpty()) {
logger.warning(this + " enterAction: actionName must not be null or empty");
return NullRootAction.INSTANCE;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " enterAction(" + actionName + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
if (session.getBeacon().isActionReportingAllowedByPrivacySettings()) {
// avoid session splitting by action count, if user opted out of action collection
recordTopActionEvent();
} else {
recordTopLevelEventInteraction();
}
return session.enterAction(actionName);
}
}
return NullRootAction.INSTANCE;
}
@Override
public void identifyUser(String userTag) {
if (logger.isDebugEnabled()) {
logger.debug(this + " identifyUser(" + userTag + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
session.identifyUser(userTag);
lastUserTag = userTag;
}
}
}
@Override
public void reportCrash(String errorName, String reason, String stacktrace) {
if (errorName == null || errorName.isEmpty()) {
logger.warning(this + " reportCrash: errorName must not be null or empty");
return;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " reportCrash(" + errorName + ", " + reason + ", " + stacktrace + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
session.reportCrash(errorName, reason, stacktrace);
// create new session after crash report
splitAndCreateNewInitialSession();
}
}
}
@Override
public void reportCrash(Throwable throwable) {
if (throwable == null) {
logger.warning(this + " reportCrash: throwable must not be null");
return;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " reportCrash(" + throwable + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
session.reportCrash(throwable);
// create new session after crash report
splitAndCreateNewInitialSession();
}
}
}
@Override
public void reportNetworkTechnology(String technology) {
if (technology != null && technology.isEmpty()) {
logger.warning(this + " reportNetworkTechnology (String): technology must be null or non-empty string");
return;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " reportNetworkTechnology (String) (" + technology + ")");
}
synchronized (lockObject) {
if (!isFinished) {
currentSession.reportNetworkTechnology(technology);
}
}
}
@Override
public void reportConnectionType(ConnectionType connectionType) {
if (logger.isDebugEnabled()) {
logger.debug(this + " reportConnectionType (ConnectionType) (" + connectionType + ")");
}
synchronized (lockObject) {
if (!isFinished) {
currentSession.reportConnectionType(connectionType);
}
}
}
@Override
public void reportCarrier(String carrier) {
if (carrier != null && carrier.isEmpty()) {
logger.warning(this + " reportCarrier (String): carrier must be null or non-empty string");
return;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " reportCarrier (String) (" + carrier + ")");
}
synchronized (lockObject) {
if (!isFinished) {
currentSession.reportCarrier(carrier);
}
}
}
@Override
public WebRequestTracer traceWebRequest(URLConnection connection) {
if (connection == null) {
logger.warning(this + " traceWebRequest (URLConnection): connection must not be null");
return NullWebRequestTracer.INSTANCE;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " traceWebRequest (URLConnection) (" + connection + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
return session.traceWebRequest(connection);
}
}
return NullWebRequestTracer.INSTANCE;
}
@Override
public WebRequestTracer traceWebRequest(String url) {
if (url == null || url.isEmpty()) {
logger.warning(this + " traceWebRequest (String): url must not be null or empty");
return NullWebRequestTracer.INSTANCE;
}
if (!WebRequestTracerStringURL.isValidURLScheme(url)) {
logger.warning(this + " traceWebRequest (String): url \"" + url + "\" does not have a valid scheme");
return NullWebRequestTracer.INSTANCE;
}
if (logger.isDebugEnabled()) {
logger.debug(this + " traceWebRequest (String) (" + url + ")");
}
synchronized (lockObject) {
if (!isFinished) {
Session session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
return session.traceWebRequest(url);
}
}
return NullWebRequestTracer.INSTANCE;
}
@Override
public void sendBizEvent(String type, Map attributes) {
if (type == null || type.isEmpty()) {
logger.warning(this + " sendBizEvent (String, Map): type must not be null or empty");
return;
}
if (attributes == null) {
attributes = new HashMap<>();
}
if (logger.isDebugEnabled()) {
logger.debug(this + " sendBizEvent(" + type + ", " + attributes.toString() + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
session.sendBizEvent(type, attributes);
}
}
}
void sendEvent(String name, Map attributes) {
if (name == null || name.isEmpty()) {
logger.warning(this + " sendEvent (String, Map): name must not be null or empty");
return;
}
if (attributes == null) {
attributes = new HashMap<>();
}
if (logger.isDebugEnabled()) {
logger.debug(this + " sendEvent(" + name + ", " + attributes.toString() + ")");
}
synchronized (lockObject) {
if (!isFinished) {
SessionImpl session = getOrSplitCurrentSessionByEvents();
recordTopLevelEventInteraction();
session.sendEvent(name, attributes);
}
}
}
@Override
public void end() {
if (logger.isDebugEnabled()) {
logger.debug(this + " end()");
}
synchronized (lockObject) {
if (isFinished) {
return;
}
isFinished = true;
}
closeChildObjects();
parent.onChildClosed(this);
sessionWatchdog.removeFromSplitByTimeout(this);
}
/**
* Close all child objects of this {@link SessionProxyImpl} which are still open.
*/
void closeChildObjects() {
List childObjects = getCopyOfChildObjects();
for (OpenKitObject childObject : childObjects) {
if (childObject instanceof SessionImpl) {
// child object is a session - special treatment is needed for sessions
SessionImpl childSession = (SessionImpl) childObject;
// end the child session and send the end session event
// if the child session is the current session
childSession.end(childSession == currentSession);
} else {
closeChildObject(childObject);
}
}
}
/**
* Close single child object.
*
* @param childObject Child object to close.
*/
private void closeChildObject(OpenKitObject childObject) {
try {
childObject.close();
} catch (IOException e) {
// should not happen, nevertheless let's log an error
logger.error(this + "Caught IOException while closing OpenKitObject (" + childObject + ")", e);
}
}
/**
* Indicates whether this session proxy was finished or is still open.
*/
public boolean isFinished() {
synchronized (lockObject) {
return isFinished;
}
}
@Override
public void close() {
end();
}
@Override
void onChildClosed(OpenKitObject childObject) {
synchronized (lockObject) {
removeChildFromList(childObject);
if (childObject instanceof SessionImpl) {
sessionWatchdog.dequeueFromClosing((SessionImpl) childObject);
}
}
}
/**
* Returns the number of top level action calls which were made to the current session. Intended to be used by unit
* tests only.
*/
int getTopLevelActionCount() {
synchronized (lockObject) {
return topLevelActionCount;
}
}
/**
* Returns the time when the last top level event was called. Intended to be used by unit tests only.
*/
long getLastInteractionTime() {
synchronized (lockObject) {
return lastInteractionTime;
}
}
/**
* Returns the server configuration of this session proxy. Intended to be used by unit tests only.
*/
ServerConfiguration getServerConfiguration() {
return serverConfiguration;
}
/**
* Returns the current active session or creates a new session if {@link #isSessionSplitByEventsRequired()}.
*/
private SessionImpl getOrSplitCurrentSessionByEvents() {
if (isSessionSplitByEventsRequired()) {
closeOrEnqueueCurrentSessionForClosing();
createSplitSessionAndMakeCurrent(serverConfiguration);
reTagCurrentSession();
}
return currentSession;
}
/**
* Indicates if the maximum number of top level actions is reached and session splitting by events needs to be
* performed.
*/
private boolean isSessionSplitByEventsRequired() {
if (serverConfiguration == null || !serverConfiguration.isSessionSplitByEventsEnabled()) {
return false;
}
return serverConfiguration.getMaxEventsPerSession() <= topLevelActionCount;
}
/**
* Will end the current active session, enque the old one for closing, and create a new session.
*
*
* The new session is created using the {@see #createInitialSession}.
*
*
*
* This method must be called only when the {@link #lockObject} is held.
*
*/
private void splitAndCreateNewInitialSession() {
closeOrEnqueueCurrentSessionForClosing();
// create a completely new SessionImpl
sessionCreator.reset();
createInitialSessionAndMakeCurrent(serverConfiguration);
reTagCurrentSession();
}
private void closeOrEnqueueCurrentSessionForClosing() {
// for grace period use half of the idle timeout
// or fallback to session interval if not configured
int closeGracePeriodInMillis = serverConfiguration.getSessionTimeoutInMilliseconds() > 0
? serverConfiguration.getSessionTimeoutInMilliseconds() / 2
: serverConfiguration.getSendIntervalInMilliseconds();
sessionWatchdog.closeOrEnqueueForClosing(currentSession, closeGracePeriodInMillis);
}
/**
* Will end the current active session and start a new one but only if the following conditions are met:
*
* - this session proxy is not {@link #isFinished() finished}.
* -
* session splitting by idle timeout is enabled and the current session was idle for longer than the
* configured timeout.
*
* -
* session splitting by maximum session duration is enabled and the session was open for longer than the
* maximum configured session duration.
*
*
*
* @return the time when the session might be split next. This can either be the time when the maximum session
* duration is reached or the time when the idle timeout expires. In case this session proxy is finished, {@code -1}
* is returned.
*/
public long splitSessionByTime() {
synchronized (lockObject) {
if (isFinished()) {
return -1;
}
long nextSplitTime = calculateNextSplitTime();
long now = timingProvider.provideTimestampInMilliseconds();
if (nextSplitTime < 0 || now < nextSplitTime) {
return nextSplitTime;
}
splitAndCreateNewInitialSession();
return calculateNextSplitTime();
}
}
/**
* Calculates and returns the next point in time when this session is to be split. The returned time might either be
*
* - the time when the session expires after the max. session duration elapsed.
* - the time when the session expires after being idle.
*
* depending on which happens earlier.
*/
private long calculateNextSplitTime() {
if (serverConfiguration == null) {
return -1;
}
boolean splitByIdleTimeout = serverConfiguration.isSessionSplitByIdleTimeoutEnabled();
boolean splitBySessionDuration = serverConfiguration.isSessionSplitBySessionDurationEnabled();
long idleTimeOut = lastInteractionTime + serverConfiguration.getSessionTimeoutInMilliseconds();
long sessionMaxTime = currentSession.getBeacon().getSessionStartTime()
+ serverConfiguration.getMaxSessionDurationInMilliseconds();
if (splitByIdleTimeout && splitBySessionDuration) {
return Math.min(idleTimeOut, sessionMaxTime);
} else if (splitByIdleTimeout) {
return idleTimeOut;
} else if (splitBySessionDuration) {
return sessionMaxTime;
}
return -1;
}
private void createInitialSessionAndMakeCurrent(ServerConfiguration initialServerConfig) {
createAndAssignCurrentSession(initialServerConfig, null);
}
private void createSplitSessionAndMakeCurrent(ServerConfiguration updatedServerConfig) {
createAndAssignCurrentSession(null, updatedServerConfig);
}
/**
* Creates a new session and adds it to the beacon sender. The top level action count is reset to zero and the last
* interaction time is set to the current timestamp.
*
*
* In case the given {@code initialServerConfig} is not null, the new session will be initialized with this server
* configuration. The created session however will not be in state {@link SessionState#isConfigured() configured},
* meaning new session requests will be performed for this session.
*
*
* In case the given {@code updatedServerConfig} is not null, the new session will be updated with this server
* configuration. The created session will be in state {@link SessionState#isConfigured()}, meaning new session
* requests will be omitted.
*
*
* @param initialServerConfig the server configuration with which the session will be initialized. Can be {@code null}.
* @param updatedServerConfig the server configuration with which the session will be updated. Can be {@code null}.
*/
private void createAndAssignCurrentSession(ServerConfiguration initialServerConfig, ServerConfiguration updatedServerConfig) {
SessionImpl session = sessionCreator.createSession(this);
Beacon beacon = session.getBeacon();
beacon.setServerConfigurationUpdateCallback(this);
storeChildInList(session);
lastInteractionTime = beacon.getSessionStartTime();
topLevelActionCount = 0;
if (initialServerConfig != null) {
session.initializeServerConfiguration(initialServerConfig);
}
if (updatedServerConfig != null) {
session.updateServerConfiguration(updatedServerConfig);
}
synchronized (lockObject) {
// synchronize access
currentSession = session;
}
this.beaconSender.addSession(session);
}
private void recordTopLevelEventInteraction() {
lastInteractionTime = timingProvider.provideTimestampInMilliseconds();
}
private void recordTopActionEvent() {
++topLevelActionCount;
recordTopLevelEventInteraction();
}
private void reTagCurrentSession() {
if (lastUserTag == null || lastUserTag.length() == 0 || currentSession == null) {
return;
}
currentSession.identifyUser(lastUserTag);
}
@Override
public void onServerConfigurationUpdate(ServerConfiguration serverConfig) {
synchronized (lockObject) {
if (serverConfiguration != null) {
serverConfiguration = serverConfiguration.merge(serverConfig);
return;
}
serverConfiguration = serverConfig;
if (isFinished()) {
return;
}
if (serverConfiguration.isSessionSplitBySessionDurationEnabled() ||
serverConfiguration.isSessionSplitByIdleTimeoutEnabled()) {
sessionWatchdog.addToSplitByTimeout(this);
}
}
}
@Override
public String toString() {
Beacon beacon = currentSession.getBeacon();
return getClass().getSimpleName()
+ " [sn=" + beacon.getSessionNumber() + ", seq=" + beacon.getSessionSequenceNumber() + "]";
}
}