Skip to content

Commit

Permalink
Add browser propagation API (#1615)
Browse files Browse the repository at this point in the history
* add GlobalPropagationDataStorage as well as ExporterService and HttpServlet

* integrate BrowserPropagationHttpExporterService into configuration

* add additional browser-propagation to DataSettings

* fix down-propagation of disabled browserPropagationData

* change data-format of BrowserPropagationServlet to EntrySet

* little refactor

* update DynamicallyActivatableServiceObserverTest

* add BrowserPropagation tests

* update documentation

* add requested changes

* add BrowserPropagationSessionStorage

* add scheduled method to clean up BrowserPropagationDataStorages

* fix makeActive()

* add requested changed

* add limitations to keys and values of data storages

* disable scheduler test

* enable runtime updates of the session limit
  • Loading branch information
EddeCCC authored Aug 2, 2023
1 parent c01e890 commit fcaecd7
Show file tree
Hide file tree
Showing 25 changed files with 1,158 additions and 11 deletions.
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ resilience4jVersion=1.7.0

javassistVersion=3.29.2-GA

eclipseJettyVersion=9.4.51.v20230217

opencensusInfluxdbExporter=1.2

influxdbJavaVersion=2.23
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public interface InternalInspectitContext extends AutoCloseable, InspectitContex
*/
String REMOTE_PARENT_SPAN_CONTEXT_KEY = "remote_parent_span_context";

/**
* Special data key which stores the remote session id if any is present
* The remains the same within one trace. Usually the data key will be down-propagated.
* The data will be cleared as soon as the root span is closed
*/
String REMOTE_SESSION_ID = "remote_session_id";

/**
* Makes this context the active one.
* This means all new contexts created from this point will use this context as a parent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Data;
import lombok.NoArgsConstructor;
import rocks.inspectit.ocelot.config.model.exporters.tags.TagsExporterSettings;
import rocks.inspectit.ocelot.config.model.exporters.trace.TraceExportersSettings;
import rocks.inspectit.ocelot.config.model.exporters.metrics.MetricsExportersSettings;

Expand All @@ -20,4 +21,7 @@ public class ExportersSettings {
@Valid
private TraceExportersSettings tracing;

@Valid
private TagsExporterSettings tags;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package rocks.inspectit.ocelot.config.model.exporters.tags;

import lombok.Data;
import rocks.inspectit.ocelot.config.model.exporters.ExporterEnabledState;

/**
* Settings for the HTTP-server tags exporter.
*/
@Data
public class HttpExporterSettings {

/**
* Whether the exporter should be started.
*/
private ExporterEnabledState enabled;

/**
* The host of the HTTP-server
*/
private String host;

/**
* The port of the HTTP-server
*/
private int port;

/**
* The path for the endpoint of the HTTP-server
*/
private String path;

/**
* How many sessions can be stored at the same time
*/
private int sessionLimit;

/**
* How long the data should be stored in the server
*/
private int timeToLive;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package rocks.inspectit.ocelot.config.model.exporters.tags;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.Valid;

@Data
@NoArgsConstructor
public class TagsExporterSettings {

@Valid
private HttpExporterSettings http;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public class DataSettings {
*/
private PropagationMode downPropagation;

/**
* Defines, if data is allowed to be propagated to browser
*/
private Boolean browserPropagation;

/**
* Defines whether this data is visible as an OpenCensus Tag.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,21 @@ inspectit:
# compression method
compression: NONE
# timeout, i.e., maximum time the OTLP exporter will wait for each batch export
timeout: 10s
timeout: 10s

# settings for tags exporters
tags:
# settings for the http-server exporter
http:
enabled: DISABLED
# the host of the http-server
host: 0.0.0.0
# the port of the http-server
port: 9000
# the path for the endpoint of the http-server
path: "/inspectit"
# how many sessions can be stored at the same time
# Additional limitations: key-length -> 128, value-length -> 2048, attribute-count -> 128
session-limit: 100
# how long the data should be stored in the server in seconds
time-to-live: 300
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ inspectit:
data:
# used for storing a received remote span id
remote_parent_span_context:
down-propagation: JVM_LOCAL
down-propagation: JVM_LOCAL
remote_session_id:
down-propagation: JVM_LOCAL
6 changes: 6 additions & 0 deletions inspectit-ocelot-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ dependencies {
// logstash support (JSON logging)
"net.logstash.logback:logstash-logback-encoder:${logstashLogBackEncoderVersion}",

// jetty HTTP server for REST-API
// we use jetty instead of Spring-Boot-Web since it's more lightweight and
// it 's easier to adapt changes from the config-server
"org.eclipse.jetty:jetty-server:${eclipseJettyVersion}",
"org.eclipse.jetty:jetty-servlet:${eclipseJettyVersion}",

// utils
"org.apache.commons:commons-lang3",
"org.apache.commons:commons-math3:${commonsMathVersion}",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package rocks.inspectit.ocelot.core.exporter;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import rocks.inspectit.ocelot.config.model.InspectitConfig;
import rocks.inspectit.ocelot.config.model.exporters.tags.HttpExporterSettings;
import rocks.inspectit.ocelot.core.instrumentation.browser.BrowserPropagationSessionStorage;
import rocks.inspectit.ocelot.core.service.DynamicallyActivatableService;

import javax.servlet.http.HttpServlet;
import java.net.InetSocketAddress;

/**
* Tags HTTP-Server to "export" data-tags to browsers
* The server contains two endpoints:
* 1. GET: To query propagation data
* 2. PUT: To overwrite propagation data
*/

@Slf4j
@Component
@EnableScheduling
public class BrowserPropagationHttpExporterService extends DynamicallyActivatableService {
private Server server;
private BrowserPropagationSessionStorage sessionStorage;
private BrowserPropagationServlet httpServlet;

/**
* Stores a reference of the InspectITConfig to enable runtime updates of the session limit
*/
private InspectitConfig inspectitConfig;

/**
* Delay to rerun the scheduled method after the method finished in milliseconds
*/
private static final int FIXED_DELAY = 10000;

/**
* Time to live for browser propagation data in seconds
*/
private int timeToLive = 300;

public BrowserPropagationHttpExporterService() {
super("exporters.tags.http");
}

@Override
protected boolean checkEnabledForConfig(InspectitConfig configuration) {
return !configuration.getExporters()
.getTags()
.getHttp()
.getEnabled()
.isDisabled();
}

@Override
protected boolean doEnable(InspectitConfig configuration) {
HttpExporterSettings settings = configuration.getExporters().getTags().getHttp();

String host = settings.getHost();
int port = settings.getPort();
String path = settings.getPath();
int sessionLimit = settings.getSessionLimit();
timeToLive = settings.getTimeToLive();
sessionStorage = BrowserPropagationSessionStorage.getInstance();
sessionStorage.setSessionLimit(sessionLimit);
httpServlet = new BrowserPropagationServlet();
inspectitConfig = configuration;

return startServer(host, port, path, httpServlet);
}

@Override
protected boolean doDisable() {
if(server != null) {
try {
log.info("Stopping Tags HTTP-Server");
server.stop();
sessionStorage.clearDataStorages();
} catch (Exception e) {
log.error("Error disabling Tags HTTP-Server", e);
}
}
return true;
}

protected boolean startServer(String host, int port, String path, HttpServlet servlet) {
server = new Server(new InetSocketAddress(host, port));
String contextPath = "";
ServletContextHandler contextHandler = new ServletContextHandler(server, contextPath);
contextHandler.addServlet(new ServletHolder(servlet), path);
server.setStopAtShutdown(true);

try {
log.info("Starting Tags HTTP-Server on {}:{}{} ", host, port, path);
server.start();
} catch (Exception e) {
log.warn("Starting of Tags HTTP-Server failed");
return false;
}
return true;
}

/**
* Updates the session storage:
* 1. Browser propagation data is cached for a specific amount of time (timeToLive)
* If the time expires, clean up the storage
* 2. Update the session limit
* Note that this will not delete any active sessions, if the new session limit is exceeded
*/
@Scheduled(fixedDelay = FIXED_DELAY)
public void updateSessionStorage() {
if(httpServlet == null) return;
sessionStorage.cleanUpData(timeToLive);

if(inspectitConfig == null) return;
int sessionLimit = inspectitConfig.getExporters().getTags().getHttp().getSessionLimit();
sessionStorage.setSessionLimit(sessionLimit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package rocks.inspectit.ocelot.core.exporter;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import rocks.inspectit.ocelot.core.instrumentation.browser.BrowserPropagationDataStorage;
import rocks.inspectit.ocelot.core.instrumentation.browser.BrowserPropagationSessionStorage;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* REST-API to expose browser propagation data
* Additionally, data can be overwritten from outside
* To access a data storage, a sessionID has to be provided, which references a data storage
* <p>
* The expected data format to receive and export data are EntrySets, for example:
* [{"key1": "123"}, {"key2": "321"}]
*/
@Slf4j
public class BrowserPropagationServlet extends HttpServlet {

private final ObjectMapper mapper;
private final BrowserPropagationSessionStorage sessionStorage;

public BrowserPropagationServlet() {
mapper = new ObjectMapper();
sessionStorage = BrowserPropagationSessionStorage.getInstance();
}

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.debug("Tags HTTP-server received GET-request");
String sessionID = request.getHeader("cookie");
if(sessionID == null) {
log.warn("Request misses session ID");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
else {
BrowserPropagationDataStorage dataStorage = sessionStorage.getDataStorage(sessionID);

if(dataStorage == null) {
log.warn("Data storage with session id " + sessionID + " not found");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
else {
dataStorage.updateTimestamp(System.currentTimeMillis());
Map<String, Object> propagationData = dataStorage.readData();
String res = mapper.writeValueAsString(propagationData.entrySet());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(res);
}
}
}

@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) {
log.debug("Tags HTTP-server received PUT-request");
String sessionID = request.getHeader("cookie");
if(sessionID == null) {
log.warn("Request misses session ID");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
else {
BrowserPropagationDataStorage dataStorage = sessionStorage.getDataStorage(sessionID);

if(dataStorage == null) {
log.warn("Data storage with session id " + sessionID + " not found");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
else {
dataStorage.updateTimestamp(System.currentTimeMillis());
Map<String, String> newPropagationData = getRequestBody(request);
if(newPropagationData != null) {
dataStorage.writeData(newPropagationData);
response.setStatus(HttpServletResponse.SC_OK);
}
else response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
}

private Map<String, String> getRequestBody(HttpServletRequest request) {
try (BufferedReader reader = request.getReader()) {
Set<Map.Entry<String,String>> entrySet = mapper.readValue(reader, new TypeReference<Set<Map.Entry<String,String>>>() {});
return entrySet.stream()
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} catch (Exception e) {
log.info("Request failed");
return null;
}
}
}
Loading

0 comments on commit fcaecd7

Please sign in to comment.