From 7d2a17d03cb738f91dd7f056a323870e99a04232 Mon Sep 17 00:00:00 2001 From: Eduard Schander <66794307+EddeCCC@users.noreply.github.com> Date: Thu, 21 Sep 2023 13:42:01 +0200 Subject: [PATCH] Server timing header (#1617) * add wrapper classes * inject wrapper classes and add trace context methods to InspectitContext * adjust NoopContext * little refactor * update dependencies * update snakeyaml in agent * change snakeyaml back to 1.33 * change wiremock back to 2.25.0 * split guava-versions * update documentation, because of ticket 290 * add createTransactionContext() * remove unnecessary changes * rename function * add tests * add requested changes and documentation * add configurable session-id-key * fix cors-errors and add tests * remove session-id-header from ContextPropagation if exporter is disabled * update documentation * fix test * remove TestPropertySource * add check for allowed origins * add error logging for browser propagation with disabled exporter --- .../build.gradle | 4 +- .../build.gradle | 4 +- gradle.properties | 14 +- inspectit-ocelot-agent/build.gradle | 3 + .../bootstrap/context/noop/NoopContext.java | 5 + .../noop/NoopLogTraceCorrelator.java | 3 +- .../bootstrap/exposed/InspectitContext.java | 9 + .../exporters/tags/HttpExporterSettings.java | 12 + .../ocelot/config/default/exporters.yml | 7 +- .../actions/http/servlet-api.yml | 25 ++ inspectit-ocelot-core/build.gradle | 4 +- ...BrowserPropagationHttpExporterService.java | 29 +- .../exporter/BrowserPropagationServlet.java | 124 ++++++--- .../BrowserPropagationSessionStorage.java | 11 +- .../context/ContextPropagationUtil.java | 35 +++ .../context/InspectitContextImpl.java | 57 +++- .../propagation/BrowserPropagationUtil.java | 57 ++++ .../span/ContinueOrStartSpanAction.java | 13 +- ...PropagationHttpExporterServiceIntTest.java | 152 ++++++++--- .../BrowserPropagationDataStorageTest.java | 28 +- .../BrowserPropagationSessionStorageTest.java | 2 +- .../context/InspectitContextImplTest.java | 13 + .../BrowserPropagationUtilTest.java | 45 +++ .../span/ContinueOrStartSpanActionTest.java | 63 +++++ .../docs/getting-started/installation.md | 2 + .../docs/instrumentation/rules.md | 89 +++++- .../docs/tags/tags-exporters.md | 257 +++++++++++++++++- 27 files changed, 936 insertions(+), 131 deletions(-) create mode 100644 inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtil.java create mode 100644 inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtilTest.java diff --git a/components/inspectit-ocelot-configdocsgenerator/build.gradle b/components/inspectit-ocelot-configdocsgenerator/build.gradle index bc1de9c700..b0d9a69aa8 100644 --- a/components/inspectit-ocelot-configdocsgenerator/build.gradle +++ b/components/inspectit-ocelot-configdocsgenerator/build.gradle @@ -15,7 +15,7 @@ dependencies { 'org.mockito:mockito-junit-jupiter', "org.assertj:assertj-core", - "com.google.guava:guava:${guavaVersion}" + "com.google.guava:guava:${guavaVersionConfigServer}" ) // This dependency is used by the application. @@ -24,6 +24,8 @@ dependencies { "ch.qos.logback:logback-classic", "org.apache.commons:commons-lang3", "commons-beanutils:commons-beanutils:${commonsBeanUtilsVersion}", + // Update dependency, due to Out-of-Support + "org.apache.commons:commons-collections4:${commonsCollectionsVersion}", "org.springframework.boot:spring-boot-starter-web", // override snakeyaml due to vulnerabilities in v1.29 used by the SpringBoot version used in this module diff --git a/components/inspectit-ocelot-configurationserver/build.gradle b/components/inspectit-ocelot-configurationserver/build.gradle index caaf1f2d9b..d600575535 100644 --- a/components/inspectit-ocelot-configurationserver/build.gradle +++ b/components/inspectit-ocelot-configurationserver/build.gradle @@ -141,7 +141,7 @@ dependencies { 'org.apache.httpcomponents:httpclient', //Required for PATCH-Requests - "org.xerial:sqlite-jdbc", + "org.xerial:sqlite-jdbc:${sqliteVersion}", "com.github.gwenn:sqlite-dialect:${sqliteDialect}", "io.jsonwebtoken:jjwt-api:${jsonWebTokenVersion}", "io.jsonwebtoken:jjwt-impl:${jsonWebTokenVersion}", @@ -152,7 +152,7 @@ dependencies { "org.eclipse.jgit:org.eclipse.jgit:${eclipseJgitVersion}", "org.eclipse.jgit:org.eclipse.jgit.ssh.jsch:${eclipseJgitVersion}", "com.google.code.gson:gson", - "com.google.guava:guava:${guavaVersion}", + "com.google.guava:guava:${guavaVersionConfigServer}", // swagger "org.springdoc:springdoc-openapi-ui:${springdocOopenapiUiVersion}", diff --git a/gradle.properties b/gradle.properties index 7856c2b600..2b8c9841a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ ### https://docs.spring.io/spring-boot/docs/${springBootVersion}/reference/html/dependency-versions.html # 2.7 is the latest release line which runs on Java 8 -springBootVersion=2.7.12 +springBootVersion=2.7.15 # Ensure to adapt the netty version when changing the OpenCensus version openCensusVersion=0.31.1 @@ -21,11 +21,14 @@ tcnativeVersion=2.0.56.Final openTelemetryVersion=1.25.0 openTelemetryAlphaVersion=1.25.0-alpha opentelemetryProtoVersion=1.7.1-alpha +# update okio, due to security concerns +okioVersion=3.5.0 grpcVersion=1.43.1 # snakeYaml version # We use a newer version as spring-boot because of Security issues +# Version 2.* is not working with the current jackson-dependency of spring 2 snakeYamlVersion=1.33 jsonWebTokenVersion=0.11.5 @@ -33,6 +36,7 @@ jsonWebTokenVersion=0.11.5 # If possible keep this version in sync with the assertj-core version used by spring-boot assertjGuavaVersion=3.24.2 +sqliteVersion=3.43.0.0 sqliteDialect=0.1.4 # Starting with version 6.x requires Java 11 @@ -40,6 +44,7 @@ eclipseJgitVersion=5.13.1.202206130422-r springdocOopenapiUiVersion=1.7.0 +# Wiremock 3.* doesn't support java 8 wiremockVersion=2.35.0 # We need to use this version, because some database setups do not work @@ -77,12 +82,13 @@ protobufJavaUtilVersion=3.22.3 logUnitVersion=1.1.3 commonsBeanUtilsVersion=1.9.4 - +commonsCollectionsVersion=4.4 commonsIoVersion=2.11.0 - commonsMathVersion=3.6.1 -guavaVersion=31.1-jre +guavaVersionConfigServer=32.1.2-jre +# ocelot-agent and ocelot-core cannot update guava, because of wiremock +guavaVersionAgent=31.1-jre ### gradle plugin versions ### Check for newer version at https://plugins.gradle.org/ diff --git a/inspectit-ocelot-agent/build.gradle b/inspectit-ocelot-agent/build.gradle index b8b6a1673e..49bb26cea7 100644 --- a/inspectit-ocelot-agent/build.gradle +++ b/inspectit-ocelot-agent/build.gradle @@ -45,6 +45,7 @@ dependencies { "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}", "io.opentelemetry:opentelemetry-opencensus-shim:${openTelemetryAlphaVersion}", "io.opencensus:opencensus-impl:${openCensusVersion}", + "com.google.guava:guava:${guavaVersionAgent}", ) annotationProcessor "org.projectlombok:lombok" testImplementation ( @@ -137,6 +138,7 @@ dependencies { systemTestImplementation( "io.opencensus:opencensus-testing:${openCensusVersion}", "io.opencensus:opencensus-api:${openCensusVersion}", + "com.google.guava:guava:${guavaVersionAgent}", // for InMemorySpanExporter platform("io.opentelemetry:opentelemetry-bom:${openTelemetryVersion}"), @@ -151,6 +153,7 @@ dependencies { "org.assertj:assertj-core", "org.awaitility:awaitility", "com.github.tomakehurst:wiremock-jre8:${wiremockVersion}", + "org.yaml:snakeyaml:${snakeYamlVersion}", "org.apache.httpcomponents:httpclient", // Switching to spring-boot dependency management excludes somehow magically the transitive diff --git a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/context/noop/NoopContext.java b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/context/noop/NoopContext.java index 57257605cb..3b6971c44a 100644 --- a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/context/noop/NoopContext.java +++ b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/context/noop/NoopContext.java @@ -27,6 +27,11 @@ public Iterable> getData() { return Collections.emptyMap().entrySet(); } + @Override + public String createRemoteParentContext() { + return null; + } + @Override public void makeActive() { } diff --git a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/correlation/noop/NoopLogTraceCorrelator.java b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/correlation/noop/NoopLogTraceCorrelator.java index 25be42aceb..775b4d75ce 100644 --- a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/correlation/noop/NoopLogTraceCorrelator.java +++ b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/correlation/noop/NoopLogTraceCorrelator.java @@ -11,7 +11,8 @@ public class NoopLogTraceCorrelator implements LogTraceCorrelator { @Override public AutoCloseable startCorrelatedSpanScope(Supplier spanScopeStarter) { - return spanScopeStarter.get(); + if(spanScopeStarter != null) return spanScopeStarter.get(); + return null; } @Override diff --git a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/exposed/InspectitContext.java b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/exposed/InspectitContext.java index 95558d20f0..1e04fc4690 100644 --- a/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/exposed/InspectitContext.java +++ b/inspectit-ocelot-bootstrap/src/main/java/rocks/inspectit/ocelot/bootstrap/exposed/InspectitContext.java @@ -38,6 +38,15 @@ public interface InspectitContext { */ Iterable> getData(); + /** + * This function should be called in the entry- or pre-entry-phase, to allow the created span to use the context + * + * Creates a SpanContext locally, which the current InspectitContext can use as a remote-parent-context, + * as long as no REMOTE_PARENT_SPAN_CONTEXT_KEY was specified earlier by down-propagation + * + * @return The trace context of the created SpanContext in the W3C-format + */ + String createRemoteParentContext(); /** * Generates a map representing the globally down-propagated data stored in this context. diff --git a/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/exporters/tags/HttpExporterSettings.java b/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/exporters/tags/HttpExporterSettings.java index 47f8b866b7..fa0ff9a535 100644 --- a/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/exporters/tags/HttpExporterSettings.java +++ b/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/exporters/tags/HttpExporterSettings.java @@ -3,6 +3,8 @@ import lombok.Data; import rocks.inspectit.ocelot.config.model.exporters.ExporterEnabledState; +import java.util.List; + /** * Settings for the HTTP-server tags exporter. */ @@ -29,11 +31,21 @@ public class HttpExporterSettings { */ private String path; + /** + * List of allowed Orgins, which are able to access the HTTP-server + */ + private List allowedOrigins; + /** * How many sessions can be stored at the same time */ private int sessionLimit; + /** + * Header, which will be read during browser-propagation to receive the session-ID + */ + private String sessionIdHeader; + /** * How long the data should be stored in the server */ diff --git a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/exporters.yml b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/exporters.yml index afd787ccf5..ebc822236c 100644 --- a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/exporters.yml +++ b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/exporters.yml @@ -103,16 +103,21 @@ inspectit: # settings for tags exporters tags: # settings for the http-server exporter + # note that this server does not provide any encryption or performs any authentication http: enabled: DISABLED # the host of the http-server - host: 0.0.0.0 + host: 127.0.0.1 # the port of the http-server port: 9000 # the path for the endpoint of the http-server path: "/inspectit" + # list of allowed origins, which are able to access the http-server + allowed-origins: ["*"] # how many sessions can be stored at the same time # Additional limitations: key-length -> 128, value-length -> 2048, attribute-count -> 128 session-limit: 100 + # header, which will be read during browser-propagation to receive the session-ID + session-id-header: "Cookie" # how long the data should be stored in the server in seconds time-to-live: 300 diff --git a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/instrumentation/actions/http/servlet-api.yml b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/instrumentation/actions/http/servlet-api.yml index fec995d70b..e3eb8efc40 100644 --- a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/instrumentation/actions/http/servlet-api.yml +++ b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/instrumentation/actions/http/servlet-api.yml @@ -109,3 +109,28 @@ inspectit: } } } + + 'a_servletapi_remoteParentContext': + docs: + since: '2.5.4' + description: "Writes a parent trace context to the given HttpServletResponse's Server-Timing header" + inputs: + 'response': 'The HttpServletResponse to write to' + is-void: true + imports: + - 'javax.servlet' + - 'javax.servlet.http' + input: + 'response': 'ServletResponse' + _context: 'InspectitContext' + value-body: | + if(response instanceof HttpServletResponse) { + HttpServletResponse res = (HttpServletResponse) response; + if(!res.isCommitted()) { + String traceContext = _context.createRemoteParentContext(); + if(traceContext == null) return; + String key = "Server-Timing"; + String value = "traceparent; desc=" + traceContext; + res.addHeader(key, value); + } + } diff --git a/inspectit-ocelot-core/build.gradle b/inspectit-ocelot-core/build.gradle index caa9a8fcf6..dbf384c860 100644 --- a/inspectit-ocelot-core/build.gradle +++ b/inspectit-ocelot-core/build.gradle @@ -95,7 +95,7 @@ dependencies { "io.prometheus:simpleclient_httpserver", // this overwrites version of guava that opencensus-impl pulls in transitively, too. - "com.google.guava:guava:${guavaVersion}", + "com.google.guava:guava:${guavaVersionAgent}", // we still need the OpenCensus SDK for the metric exporters to work, as the shim only includes opencensus-impl-core "io.opencensus:opencensus-impl:${openCensusVersion}", @@ -106,6 +106,8 @@ dependencies { "io.opentelemetry:opentelemetry-exporter-jaeger-thrift", "io.opentelemetry:opentelemetry-exporter-zipkin", "io.opentelemetry:opentelemetry-exporter-otlp", + // Update okio, due to security concerns + "com.squareup.okio:okio:${okioVersion}", platform("io.opentelemetry:opentelemetry-bom-alpha:${openTelemetryAlphaVersion}"), "io.opentelemetry:opentelemetry-exporter-prometheus", diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterService.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterService.java index 3e2ca67c63..1609987ff0 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterService.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterService.java @@ -14,6 +14,7 @@ import javax.servlet.http.HttpServlet; import java.net.InetSocketAddress; +import java.util.List; /** * Tags HTTP-Server to "export" data-tags to browsers @@ -30,11 +31,6 @@ public class BrowserPropagationHttpExporterService extends DynamicallyActivatabl 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 */ @@ -65,12 +61,16 @@ protected boolean doEnable(InspectitConfig configuration) { String host = settings.getHost(); int port = settings.getPort(); String path = settings.getPath(); - int sessionLimit = settings.getSessionLimit(); timeToLive = settings.getTimeToLive(); + + int sessionLimit = settings.getSessionLimit(); sessionStorage = BrowserPropagationSessionStorage.getInstance(); sessionStorage.setSessionLimit(sessionLimit); - httpServlet = new BrowserPropagationServlet(); - inspectitConfig = configuration; + sessionStorage.setExporterActive(true); + + String sessionIdHeader = settings.getSessionIdHeader(); + List allowedOrigins = settings.getAllowedOrigins(); + httpServlet = new BrowserPropagationServlet(sessionIdHeader, allowedOrigins); return startServer(host, port, path, httpServlet); } @@ -82,6 +82,7 @@ protected boolean doDisable() { log.info("Stopping Tags HTTP-Server"); server.stop(); sessionStorage.clearDataStorages(); + sessionStorage.setExporterActive(false); } catch (Exception e) { log.error("Error disabling Tags HTTP-Server", e); } @@ -107,19 +108,13 @@ protected boolean startServer(String host, int port, String path, HttpServlet se } /** - * 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 + * Updates the session storage + * Browser propagation data is cached for a specific amount of time (timeToLive) + * If the time expires, clean up the storage */ @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); } } diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationServlet.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationServlet.java index 78d9be1e3f..a024d32b83 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationServlet.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationServlet.java @@ -11,6 +11,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -26,65 +27,116 @@ @Slf4j public class BrowserPropagationServlet extends HttpServlet { + /** + * Header, which should be used to store the session-Ids + * Default-key: "Cookie" + */ + private final String sessionIdHeader; + + /** + * List of allowed Orgins, which are able to access the HTTP-server + */ + private final List allowedOrigins; private final ObjectMapper mapper; private final BrowserPropagationSessionStorage sessionStorage; - public BrowserPropagationServlet() { - mapper = new ObjectMapper(); - sessionStorage = BrowserPropagationSessionStorage.getInstance(); + public BrowserPropagationServlet(String sessionIdHeader, List allowedOrigins) { + this.sessionIdHeader = sessionIdHeader; + this.allowedOrigins = allowedOrigins; + this.mapper = new ObjectMapper(); + this.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); + String origin = request.getHeader("Origin"); + + //If wildcard is used, allow every origin + //Alternatively, check if current origin is allowed + if(this.allowedOrigins.contains("*") || this.allowedOrigins.contains(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "GET"); + response.setHeader("Access-Control-Allow-Credentials", "true"); - if(dataStorage == null) { - log.warn("Data storage with session id " + sessionID + " not found"); - response.setStatus(HttpServletResponse.SC_NOT_FOUND); + String sessionID = request.getHeader(sessionIdHeader); + if(sessionID == null) { + log.warn("Request to Tags HTTP-server misses session ID"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } else { - dataStorage.updateTimestamp(System.currentTimeMillis()); - Map propagationData = dataStorage.readData(); - String res = mapper.writeValueAsString(propagationData.entrySet()); - response.setContentType("application/json"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(res); + 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 propagationData = dataStorage.readData(); + String res = mapper.writeValueAsString(propagationData.entrySet()); + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(res); + } } } + else response.setStatus(HttpServletResponse.SC_FORBIDDEN); } @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); + String origin = request.getHeader("Origin"); + + //If wildcard is used, allow every origin + //Alternatively, check if current origin is allowed + if(this.allowedOrigins.contains("*") || this.allowedOrigins.contains(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "PUT"); + response.setHeader("Access-Control-Allow-Credentials", "true"); - if(dataStorage == null) { - log.warn("Data storage with session id " + sessionID + " not found"); - response.setStatus(HttpServletResponse.SC_NOT_FOUND); + String sessionID = request.getHeader(sessionIdHeader); + if(sessionID == null) { + log.warn("Request to Tags HTTP-server misses session ID"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } else { - dataStorage.updateTimestamp(System.currentTimeMillis()); - Map newPropagationData = getRequestBody(request); - if(newPropagationData != null) { - dataStorage.writeData(newPropagationData); - response.setStatus(HttpServletResponse.SC_OK); + 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 newPropagationData = getRequestBody(request); + if(newPropagationData != null) { + dataStorage.writeData(newPropagationData); + response.setStatus(HttpServletResponse.SC_OK); + } + else response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } - else response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } + else response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + @Override + protected void doOptions(HttpServletRequest request, HttpServletResponse response) { + log.debug("Tags HTTP-server received OPTIONS-request"); + String origin = request.getHeader("Origin"); + String accessControlRequestMethod = request.getHeader("Access-Control-Request-Method"); + + if (origin != null && accessControlRequestMethod != null && + (this.allowedOrigins.contains("*") || this.allowedOrigins.contains(origin)) + ) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "GET, PUT"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setStatus(HttpServletResponse.SC_OK); + } + else response.setStatus(HttpServletResponse.SC_FORBIDDEN); } private Map getRequestBody(HttpServletRequest request) { @@ -93,7 +145,7 @@ private Map getRequestBody(HttpServletRequest request) { return entrySet.stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } catch (Exception e) { - log.info("Request failed"); + log.info("Request to Tags HTTP-server failed"); return null; } } diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorage.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorage.java index 5686e1de0e..aa4a53fc7c 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorage.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorage.java @@ -1,5 +1,6 @@ package rocks.inspectit.ocelot.core.instrumentation.browser; +import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -13,11 +14,17 @@ @Slf4j public class BrowserPropagationSessionStorage { - private static final int KEY_MIN_SIZE = 64; + private static final int KEY_MIN_SIZE = 16; private static final int KEY_MAX_SIZE = 512; - @Setter + @Getter @Setter private int sessionLimit = 100; + /** + * Boolean, which helps to create error messages, if browser propagation is tried, but exporter is disabled + */ + @Getter @Setter + private boolean isExporterActive = false; + private static BrowserPropagationSessionStorage instance; private final ConcurrentMap dataStorages; diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/ContextPropagationUtil.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/ContextPropagationUtil.java index e5a48c7181..b1d3e9b705 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/ContextPropagationUtil.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/ContextPropagationUtil.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import rocks.inspectit.ocelot.config.model.tracing.PropagationFormat; +import rocks.inspectit.ocelot.core.instrumentation.context.propagation.BrowserPropagationUtil; import rocks.inspectit.ocelot.core.instrumentation.context.propagation.DatadogFormat; import rocks.inspectit.ocelot.core.opentelemetry.trace.CustomIdGenerator; @@ -47,6 +48,11 @@ public class ContextPropagationUtil { public static final String CORRELATION_CONTEXT_HEADER = "Correlation-Context"; + /** + * Session-ID-key to allow browser propagation + */ + private static String SESSION_ID_HEADER = BrowserPropagationUtil.getSessionIdHeader(); + private static final String B3_HEADER_PREFIX = "X-B3-"; private static final Pattern COMMA_WITH_WHITESPACES = Pattern.compile(" *, *"); @@ -83,6 +89,7 @@ public String get(Map carrier, String key) { static { PROPAGATION_FIELDS.add(CORRELATION_CONTEXT_HEADER); + PROPAGATION_FIELDS.add(SESSION_ID_HEADER); PROPAGATION_FIELDS.addAll(B3Propagator.injectingSingleHeader().fields()); PROPAGATION_FIELDS.addAll(B3Propagator.injectingMultiHeaders().fields()); PROPAGATION_FIELDS.addAll(W3CTraceContextPropagator.getInstance().fields()); @@ -244,6 +251,16 @@ public static SpanContext readPropagatedSpanContextFromHeaderMap(Map propagationMap) { + return propagationMap.get(ContextPropagationUtil.SESSION_ID_HEADER); + } + /** * Extracts the {@link SpanContext} from the given {@code propagator} * @@ -361,4 +378,22 @@ public static void setPropagationFormat(PropagationFormat format) { propagationFormat = B3Propagator.injectingMultiHeaders(); } } + + /** + * Updates the current session-id-header used for browser propagation + * @param sessionIdHeader new session-id-header + */ + public static void setSessionIdHeader(String sessionIdHeader) { + PROPAGATION_FIELDS.remove(SESSION_ID_HEADER); + SESSION_ID_HEADER = sessionIdHeader; + PROPAGATION_FIELDS.add(SESSION_ID_HEADER); + } + + /** + * Remove session-id-header + * For example, if the tags-http-exporter is disabled and thus no session-ids need to be extracted + */ + public static void removeSessionIdHeader() { + PROPAGATION_FIELDS.remove(SESSION_ID_HEADER); + } } diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImpl.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImpl.java index b1a2728965..babb17039b 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImpl.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImpl.java @@ -2,8 +2,11 @@ import io.opencensus.tags.*; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; +import io.opentelemetry.sdk.trace.IdGenerator; import lombok.extern.slf4j.Slf4j; import lombok.val; import rocks.inspectit.ocelot.bootstrap.context.InternalInspectitContext; @@ -185,6 +188,25 @@ public class InspectitContextImpl implements InternalInspectitContext { */ private Map cachedActivePhaseDownPropagatedData = null; + /** + * This span context serves as a placeholder for a remote parent context. + * This can be useful, if the local SpanContext is created before the actual remote context. + * Thus, the remote context could not be down-propagated. + *

+ * If a remote parent context was specified, the locally created SpanContext will use it as a remote parent. + * Later on, you can transmit the remote parent context via http-response-header to your remote service and create + * a new span with the provided context. + *

+ * Note that the remote parent context will not be used as a remote parent, if a REMOTE_PARENT_SPAN_CONTEXT_KEY was + * specified by down-propagation. + */ + private SpanContext remoteParentContext; + + /** + * Session storage for all data storages that should be created for each session + */ + private BrowserPropagationSessionStorage browserPropagationSessionStorage; + /** * Data storage for all tags that should be propagated up to or down from the browser */ @@ -196,6 +218,7 @@ private InspectitContextImpl(InspectitContextImpl parent, PropagationMetaData de this.interactWithApplicationTagContexts = interactWithApplicationTagContexts; dataOverwrites = new HashMap<>(); openingThread = Thread.currentThread(); + browserPropagationSessionStorage = BrowserPropagationSessionStorage.getInstance(); if (parent == null) { postEntryPhaseDownPropagatedData = new HashMap<>(); @@ -239,6 +262,26 @@ public void setSpanScope(AutoCloseable spanScope) { currentSpanScope = spanScope; } + @Override + public String createRemoteParentContext() { + IdGenerator generator = IdGenerator.random(); + String traceId = generator.generateTraceId(); + String spanId = generator.generateSpanId(); + TraceFlags traceFlags = TraceFlags.getSampled(); + TraceState traceState = TraceState.getDefault(); + this.remoteParentContext = SpanContext.create(traceId, spanId, traceFlags, traceState); + + String traceContext = "00-" + traceId + "-" + spanId + "-" + traceFlags.asHex(); + return traceContext; + } + + /** + * @return A remote parent context, that was created via {@link #createRemoteParentContext()} + */ + public SpanContext getRemoteParentContext() { + return this.remoteParentContext; + } + /** * @return true, if {@link #setSpanScope(AutoCloseable)} was called */ @@ -269,8 +312,7 @@ public SpanContext getAndClearCurrentRemoteSpanContext() { public void makeActive() { Object currentSessionID = getData(REMOTE_SESSION_ID); if(currentSessionID != null) { - BrowserPropagationSessionStorage sessionStorage = BrowserPropagationSessionStorage.getInstance(); - browserPropagationDataStorage = sessionStorage.getOrCreateDataStorage(currentSessionID.toString()); + browserPropagationDataStorage = browserPropagationSessionStorage.getOrCreateDataStorage(currentSessionID.toString()); } @@ -432,8 +474,13 @@ public void close() { } // Write browser propagation data to storage + Map browserPropagationData = getBrowserPropagationData(dataOverwrites); if(browserPropagationDataStorage != null) - browserPropagationDataStorage.writeData(getBrowserPropagationData(dataOverwrites)); + browserPropagationDataStorage.writeData(browserPropagationData); + + //If there is browser propagation data, but exporter is disabled, write error message + if(!browserPropagationData.isEmpty() && !browserPropagationSessionStorage.isExporterActive()) + log.error("Unable to propagate data: {} Browser propagation is disabled, since no Tags-exporter is enabled", browserPropagationData); // Delete session ID after root span is closed if(parent == null) { @@ -529,8 +576,8 @@ public void readDownPropagationHeaders(Map headers) { ContextPropagationUtil.readPropagatedDataFromHeaderMap(headers, this); SpanContext remote_span = ContextPropagationUtil.readPropagatedSpanContextFromHeaderMap(headers); setData(REMOTE_PARENT_SPAN_CONTEXT_KEY, remote_span); - String sessionID = headers.get("cookie"); - if(sessionID != null) setData(REMOTE_SESSION_ID, sessionID); + String sessionId = ContextPropagationUtil.readPropagatedSessionIdFromHeaderMap(headers); + if(sessionId != null) setData(REMOTE_SESSION_ID, sessionId); } @Override diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtil.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtil.java new file mode 100644 index 0000000000..d6b7e920ff --- /dev/null +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtil.java @@ -0,0 +1,57 @@ +package rocks.inspectit.ocelot.core.instrumentation.context.propagation; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import rocks.inspectit.ocelot.core.config.InspectitConfigChangedEvent; +import rocks.inspectit.ocelot.core.config.InspectitEnvironment; +import rocks.inspectit.ocelot.core.instrumentation.context.ContextPropagationUtil; + +import javax.annotation.PostConstruct; + +/** + * Class to regulate the currently used session-id-key. + * The session-id-key is used to extract session-ids from http-request-headers to allow browser propagation. + * The session-id-key can change during runtime and needs to updated inside the PROPAGATION_FIELDS in ContextPropagationUtil. + */ +@Slf4j +@Component +public class BrowserPropagationUtil { + + @Autowired + private InspectitEnvironment env; + @Getter + private static String sessionIdHeader = "Cookie"; + + @PostConstruct + public void initialize() { + if(isBrowserPropagationEnabled()) + setSessionIdHeader(env.getCurrentConfig().getExporters().getTags().getHttp().getSessionIdHeader()); + else + ContextPropagationUtil.removeSessionIdHeader(); + } + + @EventListener + private void configEventListener(InspectitConfigChangedEvent event) { + if(isBrowserPropagationEnabled()) { + String oldSessionIdHeader = event.getOldConfig().getExporters().getTags().getHttp().getSessionIdHeader(); + String newSessionIdHeader = event.getNewConfig().getExporters().getTags().getHttp().getSessionIdHeader(); + + if(!oldSessionIdHeader.equals(newSessionIdHeader)) ContextPropagationUtil.setSessionIdHeader(newSessionIdHeader); + } + } + + @VisibleForTesting + void setSessionIdHeader(String key) { + sessionIdHeader = key; + log.info("Use of new session-id-header: " + sessionIdHeader); + ContextPropagationUtil.setSessionIdHeader(sessionIdHeader); + } + + private boolean isBrowserPropagationEnabled() { + return !env.getCurrentConfig().getExporters().getTags().getHttp().getEnabled().isDisabled(); + } +} diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanAction.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanAction.java index 83bf15c290..41615a0149 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanAction.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanAction.java @@ -124,8 +124,12 @@ private void startSpan(ExecutionContext context) { MethodReflectionInformation methodInfo = context.getHook().getMethodInformation(); String spanName = getSpanName(context, methodInfo); - // load remote parent if it exist + // load remote parent if it exists in dataOverwrites SpanContext remoteParent = ctx.getAndClearCurrentRemoteSpanContext(); + + // if no remote parent span was down-propagated, look up whether a remote parent context was created locally + if(remoteParent == null) remoteParent = ctx.getRemoteParentContext(); + boolean hasLocalParent = false; if (remoteParent == null) { Span currentSpan = Span.current(); @@ -134,11 +138,12 @@ private void startSpan(ExecutionContext context) { hasLocalParent = !(currentSpan == Span.getInvalid() || !currentSpan.getSpanContext().isValid()); } + // This is necessary, since the lambda expression needs a final value + SpanContext finalRemoteParent = remoteParent; Sampler sampler = getSampler(context); - AutoCloseable spanCtx = Instances.logTraceCorrelator.startCorrelatedSpanScope(() -> stackTraceSampler.createAndEnterSpan(spanName, remoteParent, sampler, spanKind, methodInfo, autoTrace)); + AutoCloseable spanCtx = Instances.logTraceCorrelator.startCorrelatedSpanScope(() -> stackTraceSampler.createAndEnterSpan(spanName, finalRemoteParent, sampler, spanKind, methodInfo, autoTrace)); ctx.setSpanScope(spanCtx); commonTagsToAttributesManager.writeCommonTags(Span.current(), remoteParent != null, hasLocalParent); - } } @@ -171,7 +176,7 @@ private String getSpanName(ExecutionContext context, MethodReflectionInformation name = data.toString(); } } - if (name == null) { + if (name == null && methodInfo != null) { name = methodInfo.getDeclaringClass().getSimpleName() + "." + methodInfo.getName(); } return name; diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterServiceIntTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterServiceIntTest.java index 586f0eb377..c7b1d5ce6f 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterServiceIntTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/exporter/BrowserPropagationHttpExporterServiceIntTest.java @@ -6,6 +6,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpOptions; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -17,15 +18,11 @@ import rocks.inspectit.ocelot.core.SpringTestBase; import rocks.inspectit.ocelot.core.instrumentation.browser.BrowserPropagationSessionStorage; -import javax.servlet.http.HttpServlet; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; -@TestPropertySource(properties = {"inspectit.exporters.tags.http.time-to-live=8"}) @DirtiesContext public class BrowserPropagationHttpExporterServiceIntTest extends SpringTestBase { @@ -36,8 +33,11 @@ public class BrowserPropagationHttpExporterServiceIntTest extends SpringTestBase private static final String sessionID = "test=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-4942-453a-9243-7d8422803604"; private static final String host = "127.0.0.1"; - private static int port; + private static String url; + private static final String path = "/inspectit"; + private static final String sessionIDHeader = "Cookie"; + private static final String allowedOrigin = "localhost"; @BeforeEach void prepareTest() throws IOException { @@ -52,9 +52,10 @@ void clearDataStorage() { } void startServer() throws IOException { - port = Network.getFreeServerPort(); - HttpServlet servlet = new BrowserPropagationServlet(); + int port = Network.getFreeServerPort(); + BrowserPropagationServlet servlet = new BrowserPropagationServlet(sessionIDHeader, Collections.singletonList(allowedOrigin)); exporterService.startServer(host, port, path, servlet); + url = "http://" + host + ":" + port + path; } CloseableHttpClient createHttpClient(){ @@ -75,11 +76,11 @@ void writeDataIntoStorage() { class GetEndpoint { @Test void verifyGetEndpoint() throws IOException { - String url = "http://" + host + ":" + port + path; HttpGet getRequest = new HttpGet(url); - getRequest.setHeader("cookie", sessionID); - CloseableHttpResponse response = testClient.execute(getRequest); + getRequest.setHeader("Origin", allowedOrigin); + getRequest.setHeader(sessionIDHeader, sessionID); + CloseableHttpResponse response = testClient.execute(getRequest); int statusCode = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); String responseBody = EntityUtils.toString(entity); @@ -92,27 +93,53 @@ void verifyGetEndpoint() throws IOException { @Test void verifyGetEndpointWithoutSessionID() throws IOException { - String url = "http://" + host + ":" + port + path; HttpGet getRequest = new HttpGet(url); + getRequest.setHeader("Origin", allowedOrigin); CloseableHttpResponse response = testClient.execute(getRequest); - int statusCode = response.getStatusLine().getStatusCode(); + assertThat(statusCode).isEqualTo(400); response.close(); } @Test void verifyGetEndpointWithWrongSessionID() throws IOException { - String url = "http://" + host + ":" + port + path; HttpGet getRequest = new HttpGet(url); - getRequest.setHeader("cookie", "###WrongSessionID###"); - CloseableHttpResponse response = testClient.execute(getRequest); + getRequest.setHeader("Origin", allowedOrigin); + getRequest.setHeader(sessionIDHeader, "###WrongSessionID###"); + CloseableHttpResponse response = testClient.execute(getRequest); int statusCode = response.getStatusLine().getStatusCode(); + assertThat(statusCode).isEqualTo(404); response.close(); } + + @Test + void verifyGetEndpointWithoutCorrectSessionIDKey() throws IOException { + HttpGet getRequest = new HttpGet(url); + getRequest.setHeader("Origin", allowedOrigin); + getRequest.setHeader(sessionIDHeader + "-1", sessionID); + + CloseableHttpResponse response = testClient.execute(getRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(400); + response.close(); + } + + @Test + void verifyGetEndpointWithoutAllowedOrigin() throws IOException { + HttpGet getRequest = new HttpGet(url); + getRequest.setHeader("Origin", "www.example.com"); + + CloseableHttpResponse response = testClient.execute(getRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(403); + response.close(); + } } @Nested @@ -120,12 +147,12 @@ class PutEndpoint { @Test void verifyPutEndpointWithCorrectData() throws IOException { - String url = "http://" + host + ":" + port + path; HttpPut putRequest = new HttpPut(url); String requestBody = "[{\"newKey\":\"newValue\"}]"; StringEntity requestEntity = new StringEntity(requestBody); putRequest.setEntity(requestEntity); - putRequest.setHeader("cookie", sessionID); + putRequest.setHeader("Origin", allowedOrigin); + putRequest.setHeader(sessionIDHeader, sessionID); CloseableHttpResponse response = testClient.execute(putRequest); int statusCode = response.getStatusLine().getStatusCode(); @@ -137,12 +164,12 @@ void verifyPutEndpointWithCorrectData() throws IOException { @Test void verifyPutEndpointWithIncorrectData() throws IOException { - String url = "http://" + host + ":" + port + path; HttpPut putRequest = new HttpPut(url); String requestBody = "##WrongDataFormat##"; StringEntity requestEntity = new StringEntity(requestBody); putRequest.setEntity(requestEntity); - putRequest.setHeader("cookie", sessionID); + putRequest.setHeader("Origin", allowedOrigin); + putRequest.setHeader(sessionIDHeader, sessionID); CloseableHttpResponse response = testClient.execute(putRequest); int statusCode = response.getStatusLine().getStatusCode(); @@ -153,11 +180,11 @@ void verifyPutEndpointWithIncorrectData() throws IOException { @Test void verifyPutEndpointWithoutSessionID() throws IOException { - String url = "http://" + host + ":" + port + path; HttpPut putRequest = new HttpPut(url); String requestBody = "[{\"newKey\":\"newValue\"}]"; StringEntity requestEntity = new StringEntity(requestBody); putRequest.setEntity(requestEntity); + putRequest.setHeader("Origin", allowedOrigin); CloseableHttpResponse response = testClient.execute(putRequest); int statusCode = response.getStatusLine().getStatusCode(); @@ -169,12 +196,12 @@ void verifyPutEndpointWithoutSessionID() throws IOException { @Test void verifyPutEndpointWithWrongSessionID() throws IOException { - String url = "http://" + host + ":" + port + path; HttpPut putRequest = new HttpPut(url); String requestBody = "[{\"newKey\":\"newValue\"}]"; StringEntity requestEntity = new StringEntity(requestBody); putRequest.setEntity(requestEntity); - putRequest.setHeader("cookie", "###WrongSessionID###"); + putRequest.setHeader("Origin", allowedOrigin); + putRequest.setHeader(sessionIDHeader, "###WrongSessionID###"); CloseableHttpResponse response = testClient.execute(putRequest); int statusCode = response.getStatusLine().getStatusCode(); @@ -185,21 +212,84 @@ void verifyPutEndpointWithWrongSessionID() throws IOException { } @Test - @Disabled("Fails in Github Actions") - void verifyPutEndpointTimeToLive() throws IOException, InterruptedException { - String url = "http://" + host + ":" + port + path; + void verifyPutEndpointWithoutCorrectSessionIDKey() throws IOException { HttpPut putRequest = new HttpPut(url); String requestBody = "[{\"newKey\":\"newValue\"}]"; StringEntity requestEntity = new StringEntity(requestBody); putRequest.setEntity(requestEntity); - putRequest.setHeader("cookie", sessionID); + putRequest.setHeader("Origin", allowedOrigin); + putRequest.setHeader(sessionIDHeader + "-1", sessionID); CloseableHttpResponse response = testClient.execute(putRequest); - assertThat(sessionStorage.getOrCreateDataStorage(sessionID).readData()).containsEntry("newKey", "newValue"); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(400); + assertThat(sessionStorage.getOrCreateDataStorage(sessionID).readData()).doesNotContainEntry("newKey", "newValue"); + response.close(); + } + + @Test + void verifyPutEndpointWithoutAllowedOrigin() throws IOException { + HttpPut putRequest = new HttpPut(url); + putRequest.setHeader("Origin", "www.example.com"); + + CloseableHttpResponse response = testClient.execute(putRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(403); + response.close(); + } + } + + @Nested + class OptionsEndpoint { + @Test + void verifyOptionsEndpointSuccessfulGet() throws IOException { + HttpOptions optionsRequest = new HttpOptions(url); + optionsRequest.setHeader("Origin", allowedOrigin); + optionsRequest.setHeader("access-control-request-method", "GET"); + + CloseableHttpResponse response = testClient.execute(optionsRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(200); + response.close(); + } + + @Test + void verifyOptionsEndpointSuccessfulPut() throws IOException { + HttpOptions optionsRequest = new HttpOptions(url); + optionsRequest.setHeader("Origin", allowedOrigin); + optionsRequest.setHeader("access-control-request-method", "PUT"); + + CloseableHttpResponse response = testClient.execute(optionsRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(200); + response.close(); + } + + @Test + void verifyOptionsEndpointWithMissingHeader() throws IOException { + HttpOptions optionsRequest = new HttpOptions(url); + optionsRequest.setHeader("Origin", allowedOrigin); + + CloseableHttpResponse response = testClient.execute(optionsRequest); + int statusCode = response.getStatusLine().getStatusCode(); + + assertThat(statusCode).isEqualTo(403); + response.close(); + } + + @Test + void verifyOptionsEndpointWithoutAllowedOrigin() throws IOException { + HttpOptions optionsRequest = new HttpOptions(url); + optionsRequest.setHeader("Origin", "www.example.com"); + + CloseableHttpResponse response = testClient.execute(optionsRequest); + int statusCode = response.getStatusLine().getStatusCode(); - // 10s (Clean-Up-Frequency) + 10s (Buffer) - TimeUnit.SECONDS.sleep(20); - assertThat(sessionStorage.getOrCreateDataStorage(sessionID).readData()).isEmpty(); + assertThat(statusCode).isEqualTo(403); response.close(); } } diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationDataStorageTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationDataStorageTest.java index 7b2837960f..b81179ad7a 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationDataStorageTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationDataStorageTest.java @@ -4,6 +4,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import rocks.inspectit.ocelot.core.SpringTestBase; import rocks.inspectit.ocelot.core.instrumentation.config.model.propagation.PropagationMetaData; import rocks.inspectit.ocelot.core.instrumentation.context.ContextUtil; import rocks.inspectit.ocelot.core.instrumentation.context.InspectitContextImpl; @@ -19,7 +22,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class BrowserPropagationDataStorageTest { +public class BrowserPropagationDataStorageTest extends SpringTestBase { @Mock PropagationMetaData propagation; @@ -28,15 +31,17 @@ public class BrowserPropagationDataStorageTest { Map headers; - private static final String sessionID = "test=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-4942-453a-9243-7d8422803604"; + private static final String sessionIdHeader = "Cookie"; + private static final String sessionId = "test=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-4942-453a-9243-7d8422803604"; @BeforeEach void prepareTest() { // Create session storage to store BrowserPropagationDataStorages sessionStorage = BrowserPropagationSessionStorage.getInstance(); + sessionStorage.setExporterActive(true); // Create HTTP header to pass it to the initial InspectIT-Context headers = new HashMap<>(); - headers.put("cookie", sessionID); + headers.put(sessionIdHeader, sessionId); } @AfterEach @@ -50,7 +55,7 @@ public class WriteBrowserPropagationData { @Test void verifyNoDataHasBeenWritten() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(false); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); InspectitContextImpl ctx = InspectitContextImpl.createFromCurrent(Collections.emptyMap(), propagation, false); ctx.readDownPropagationHeaders(headers); ctx.makeActive(); @@ -67,7 +72,7 @@ void verifyNoDataHasBeenWritten() { void verifyDataHasBeenWritten() { when(propagation.isPropagatedWithBrowser(anyString())).thenReturn(false); when(propagation.isPropagatedWithBrowser(eq("keyA"))).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); InspectitContextImpl ctx = InspectitContextImpl.createFromCurrent(Collections.emptyMap(), propagation, false); ctx.readDownPropagationHeaders(headers); ctx.makeActive(); @@ -87,7 +92,7 @@ void verifyDataHasBeenWritten() { void verifyDataHasBeenOverwritten() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); when(propagation.isPropagatedDownWithinJVM(any())).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); Map oldData = new HashMap<>(); oldData.put("keyA", "value0"); dataStorage.writeData(oldData); @@ -114,7 +119,7 @@ void verifyDataHasBeenOverwritten() { @Test void verifyAttributeCountLimit() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); Map dummyMap = IntStream.rangeClosed(1, 128).boxed() .collect(Collectors.toMap(i -> "key"+i, i -> "value"+i)); dataStorage.writeData(dummyMap); @@ -136,10 +141,11 @@ void verifyAttributeCountLimit() { @Test void verifyValidEntries() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); InspectitContextImpl ctx = InspectitContextImpl.createFromCurrent(Collections.emptyMap(), propagation, false); ctx.readDownPropagationHeaders(headers); ctx.makeActive(); + // Create too long key and value String dummyKey = IntStream.range(1, 130).mapToObj(i -> "x").collect(Collectors.joining()); String dummyValue = IntStream.range(1,2050).mapToObj(i -> "y").collect(Collectors.joining()); @@ -161,7 +167,7 @@ void verifyNoDownPropagation() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); when(propagation.isPropagatedDownWithinJVM(any())).thenReturn(false); when(propagation.isPropagatedDownWithinJVM(eq("remote_session_id"))).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); Map data = new HashMap<>(); data.put("keyA", "valueA"); dataStorage.writeData(data); @@ -186,7 +192,7 @@ void verifyNoDownPropagation() { void verifyDownPropagation() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); when(propagation.isPropagatedDownWithinJVM(any())).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); InspectitContextImpl ctxA = InspectitContextImpl.createFromCurrent(Collections.emptyMap(), propagation, false); Map data = new HashMap<>(); data.put("keyA", "valueA"); @@ -219,7 +225,7 @@ void verifyUpPropagation() { when(propagation.isPropagatedWithBrowser(any())).thenReturn(true); when(propagation.isPropagatedDownWithinJVM(any())).thenReturn(true); when(propagation.isPropagatedUpWithinJVM(eq("keyB"))).thenReturn(true); - BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionID); + BrowserPropagationDataStorage dataStorage = sessionStorage.getOrCreateDataStorage(sessionId); InspectitContextImpl ctxA = InspectitContextImpl.createFromCurrent(Collections.emptyMap(), propagation, false); Map data = new HashMap<>(); data.put("keyA", "valueA"); diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorageTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorageTest.java index dfd1d6a1ae..e9856cf65c 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorageTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/browser/BrowserPropagationSessionStorageTest.java @@ -7,7 +7,7 @@ public class BrowserPropagationSessionStorageTest { private static final String validSessionID = "test=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-4942-453a-9243-7d8422803604"; - private static final String anotherValidSessionID = "test=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-4942-453a-9243-92439b443924"; + private static final String anotherValidSessionID = "test=83311527d6a"; private static final String shortSessionID = "test-session"; private static final String longSessionID = "test1=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;test2=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;test3=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;test4=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;test5=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;test6=83311527d6a6de76a60a72a041808a63;b0b2b4cf=ad9fef38-49c42-45b3a-9243-7d8422803604-92439b443924;"; diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImplTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImplTest.java index afa2e07c64..c5c1794896 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImplTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/InspectitContextImplTest.java @@ -70,6 +70,19 @@ void verifyCleared() { } + @Nested + public class CreateRemoteParentContext { + @Test + void verifyTraceContextFormat() { + InspectitContextImpl ctx = InspectitContextImpl.createFromCurrent(new HashMap<>(), propagation, false); + String traceContext = ctx.createRemoteParentContext(); + String w3cFormat = "00-([0-9a-f]{32})-([0-9a-f]{16})-01"; + + assertThat(traceContext.matches(w3cFormat)).isTrue(); + ctx.close(); + } + } + @Nested public class EnterSpan { diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtilTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtilTest.java new file mode 100644 index 0000000000..ef18331c5c --- /dev/null +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/context/propagation/BrowserPropagationUtilTest.java @@ -0,0 +1,45 @@ +package rocks.inspectit.ocelot.core.instrumentation.context.propagation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.ocelot.core.instrumentation.context.ContextPropagationUtil; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class BrowserPropagationUtilTest { + + @InjectMocks + BrowserPropagationUtil browserPropagationUtil; + + final static String key = "Cookie"; + + @BeforeEach + void setUp() { + browserPropagationUtil.setSessionIdHeader(key); + } + + @Test + void verifySessionIdKeyExists() { + Set headers = ContextPropagationUtil.getPropagationHeaderNames(); + + assertThat(headers.contains(key)).isTrue(); + } + + @Test + void verifySessionIdKeyIsUpdated() { + Set headers = ContextPropagationUtil.getPropagationHeaderNames(); + assertThat(headers.contains(key)).isTrue(); + + String newKey = "NewCookie"; + browserPropagationUtil.setSessionIdHeader(newKey); + + assertThat(headers.contains(key)).isFalse(); + assertThat(headers.contains(newKey)).isTrue(); + } +} diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanActionTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanActionTest.java index 1ecc5d993f..c2efffd98a 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanActionTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/ContinueOrStartSpanActionTest.java @@ -1,6 +1,8 @@ package rocks.inspectit.ocelot.core.instrumentation.hook.actions.span; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.trace.samplers.Sampler; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,9 +10,20 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import rocks.inspectit.ocelot.bootstrap.Instances; +import rocks.inspectit.ocelot.bootstrap.correlation.LogTraceCorrelator; +import rocks.inspectit.ocelot.bootstrap.correlation.noop.NoopLogTraceCorrelator; import rocks.inspectit.ocelot.config.model.tracing.SampleMode; +import rocks.inspectit.ocelot.core.instrumentation.autotracing.StackTraceSampler; +import rocks.inspectit.ocelot.core.instrumentation.config.model.propagation.PropagationMetaData; +import rocks.inspectit.ocelot.core.instrumentation.context.InspectitContextImpl; +import rocks.inspectit.ocelot.core.instrumentation.hook.MethodHook; import rocks.inspectit.ocelot.core.instrumentation.hook.VariableAccessor; import rocks.inspectit.ocelot.core.instrumentation.hook.actions.IHookAction; +import rocks.inspectit.ocelot.core.instrumentation.hook.tags.CommonTagsToAttributesManager; +import rocks.inspectit.ocelot.core.selfmonitoring.ActionScopeFactory; + +import java.util.HashMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; @@ -77,4 +90,54 @@ void dynamicNullProbability() { } } + + @Nested + public class RemoteParentContext { + + @Mock + IHookAction.ExecutionContext executionContext; + @Mock + StackTraceSampler stackTraceSampler; + @Mock + CommonTagsToAttributesManager commonTagsToAttributesManager; + @Mock + InspectitContextImpl inspectitContext; + + MethodHook hook; + ContinueOrStartSpanAction action; + + @BeforeEach + void setUp() { + hook = MethodHook.builder().actionScopeFactory(mock(ActionScopeFactory.class)).build(); + + action = ContinueOrStartSpanAction.builder() + .continueSpanCondition(x -> false) + .startSpanCondition(x -> true) + .stackTraceSampler(stackTraceSampler) + .commonTagsToAttributesManager(commonTagsToAttributesManager) + .build(); + } + + @Test + void verifyGetRemoteParentContextCalled() { + when(executionContext.getInspectitContext()).thenReturn(inspectitContext); + when(executionContext.getHook()).thenReturn(hook); + when(inspectitContext.getAndClearCurrentRemoteSpanContext()).thenReturn(null); + + action.execute(executionContext); + + verify(inspectitContext, times(1)).getRemoteParentContext(); + } + + @Test + void verifyGetRemoteParentContextNotCalled() { + when(executionContext.getInspectitContext()).thenReturn(inspectitContext); + when(executionContext.getHook()).thenReturn(hook); + when(inspectitContext.getAndClearCurrentRemoteSpanContext()).thenReturn(SpanContext.getInvalid()); + + action.execute(executionContext); + + verify(inspectitContext, times(0)).getRemoteParentContext(); + } + } } diff --git a/inspectit-ocelot-documentation/docs/getting-started/installation.md b/inspectit-ocelot-documentation/docs/getting-started/installation.md index 8d7087c1d8..c49e0fdd6c 100644 --- a/inspectit-ocelot-documentation/docs/getting-started/installation.md +++ b/inspectit-ocelot-documentation/docs/getting-started/installation.md @@ -10,6 +10,8 @@ This section describes the installation details for the inspectIT Ocelot agent. The inspectIT Ocelot supports Java Runtime Environments in version 1.8.0 and above. You will not be able to use the agent with the lower Java versions. The agent works with different JRE distributions including Oracle, openJDK, Azul, etc. +It is recommended to always use the latest minor release of your current Java Runtime Environment version in order to ensure straightforward operation. + ## Adding the Agent to a JVM The best option for using the inspectIT Ocelot is to include it to the start of the JVM by using the `-javaagent` command-line option. diff --git a/inspectit-ocelot-documentation/docs/instrumentation/rules.md b/inspectit-ocelot-documentation/docs/instrumentation/rules.md index 97aa7324ce..dd430750c6 100644 --- a/inspectit-ocelot-documentation/docs/instrumentation/rules.md +++ b/inspectit-ocelot-documentation/docs/instrumentation/rules.md @@ -71,8 +71,9 @@ For this reason the inspectIT context implements _data propagation_. * **Up Propagation:** Data collected in your instrumented method will be visible to the methods which caused the invocation of your method. This means that all methods which lie on the call stack will have access to the data written by your method * **Browser Propagation:** An additional form of propagation. Data collected in your instrumented method will be stored inside a data storage. This storage can be exposed via a REST-API, if [_exporters.tags.http_](../tags/tags-exporters.md#http-exporter) is enabled. A browser can read this data via GET-requests. -Additionally, a browser can write data into the storage via PUT-requests. If you enabled browser- as well as down-propagation for a data key, the data written by the browser will be stored in the _inspectIT context_. -Please note, before writing or reading browser propagation data, you need to specify a session-ID inside the _inspectIT context_. After that, all data belonging to the current browser will be stored behind this session-ID. +Additionally, a browser can write data into the storage via PUT-requests. If browser- as well as down-propagation is enabled for a data key, the data written by the browser will be stored in the _inspectIT context_. +Please note, before writing or reading browser propagation data, you need to provide a session-ID inside the request-header. +After that, all data belonging to the current session will be stored behind this session-ID. For more information, see [Tags-HTTP-Exporter](../tags/tags-exporters.md#http-exporter). Up- and down propagation can also be combined: in this case then the data is attached to the control flow, meaning that it will appear as if its value will be passed around with every method call and return. @@ -718,6 +719,90 @@ In this case, the rule will first attempt to continue the existing span. Only if Again, conditions for the span continuing and span ending can be specified just like for the span starting. The properties `continue-span-conditions` and `end-span-conditions` work just like `start-span-conditions`. +#### Distributed Tracing with Remote Parent Context + +There are two ways to use a remote parent context in distributed tracing. If the remote parent context exists before the current context, +you can use _readDownPropagationHeaders()_ inside your action. If the remote parent context will be created after your current context, +you can use _createRemoteParentContext()_ inside your action. + +_readDownPropagationHeaders()_ + +The method takes a _Map_ as a parameter. This map should contain at least the trace context, which was sent by a Http request header. +The remote parent trace context should be in either B3-format, W3C-format or datadog-format. +The action, which uses this function, should be called in the pre-entry or entry phase. + +There is also a default-action in InspectIT, which uses the readDownPropagationHeaders()-function: + +```yaml + 'a_servletapi_downPropagation': + docs: + since: '1.2.1' + description: 'Reads down-propagated data from the request HTTP headers.' + is-void: true + imports: + - 'java.util' + - 'javax.servlet' + - 'javax.servlet.http' + input: + _arg0: 'ServletRequest' + _context: 'InspectitContext' + value-body: | + if (_arg0 instanceof HttpServletRequest) { + HttpServletRequest req = (HttpServletRequest) _arg0; + Collection headerKeys = _context.getPropagationHeaderNames(); + Map presentHeaders = new HashMap(); + Iterator it = headerKeys.iterator(); + while (it.hasNext()) { + String name = (String) it.next(); + java.util.Enumeration values = req.getHeaders(name); + if (values != null && values.hasMoreElements()) { + presentHeaders.put(name, String.join(",", Collections.list(values))); + } + } + _context.readDownPropagationHeaders(presentHeaders); + } +``` + +_createRemoteParentContext()_ + +The method takes no parameter. It can be used, if the remote context will be created after the current context, but should be used as a parent. +The function creates a span context, which will be used as a parent for the current context. It returns a string, which contains +the trace context of the remote parent in W3C-format. You can send the returned trace context to your remote service via http response header +and use the information to create the remote parent context. + +Note that, if a remote parent context was down propagated via _readDownPropagationHeaders()_, InspectIT will use the down propagated context as a parent +and ignore the context created with _createRemoteParentContext()_. + +The action, which uses this function, should be called in the pre-entry or entry phase. +There is also a default-action in InspectIT, which uses the createRemoteParentContext()-function: + +```yaml + 'a_servletapi_remoteParentContext': + docs: + since: '2.5.4' + description: "Writes a parent trace context to the given HttpServletResponse's Server-Timing header" + inputs: + 'response': 'The HttpServletResponse to write to.' + is-void: true + imports: + - 'javax.servlet' + - 'javax.servlet.http' + input: + 'response': 'ServletResponse' + _context: 'InspectitContext' + value-body: | + if(response instanceof HttpServletResponse) { + HttpServletResponse res = (HttpServletResponse) response; + if(!res.isCommitted()) { + String traceContext = _context.createRemoteParentContext(); + if(traceContext == null) return; + String key = "Server-Timing"; + String value = "traceparent; desc=" + traceContext; + res.addHeader(key, value); + } + } +``` + ### Modularizing Rules When writing complex instrumentation, it can happen that you want to reuse parts of your instrumentation across different rules. diff --git a/inspectit-ocelot-documentation/docs/tags/tags-exporters.md b/inspectit-ocelot-documentation/docs/tags/tags-exporters.md index 375089fa23..46674a8d63 100644 --- a/inspectit-ocelot-documentation/docs/tags/tags-exporters.md +++ b/inspectit-ocelot-documentation/docs/tags/tags-exporters.md @@ -17,22 +17,255 @@ One GET-endpoint to expose data to external applications and one PUT-endpoint to The server is by default started on the port `9000` and data can then be accessed or written by calling 'http://localhost:9000/inspectit' -Data will always be stored behind a provided session-ID to ensure data correlation with its browser. The session-ID is read from the `cookie`-header. You cannot access any data inside the HTTP-server without an existing session-ID. -A HTTP-server can only store a specific amount of sessions, which can be configured in the configuration server. The _session-limit_ can be updated at runtime. However, this will not delete any active sessions, if the new session limit is exceeded. Sessions will be deleted after their _time-to-live_ is expired. +#### Production environment +The Tags HTTP exporter does not provide any encryption of data and does not perform any authentication. +Thus, this server should not be exposed directly to the public in a production environment. +It is recommended to set up a proxy in front of this server, which handles encryption and authentication. + +The server performs authorization with checking, whether the request origin is allowed to access the server. +Additionally, every request has to provide a session-ID to access their own session data. + +#### Session identification + +Data tags will always be stored behind a provided session-ID to ensure data correlation with its browser. +The session-ID will be read from a specific request-header. The _**session-id-header**_-property in the HTTP-exporter allows +to specify, which exact header should be used to read the session-ID from. + +The default-instrumentation of InspectIT will check the specified _session-id-header_ for a valid session-ID. +Thus, there is no additional configuration necessary to read session-ID from HTTP-headers. +The length of a session-ID is restricted to a minimum of 64 characters and a maximum of 512 characters. + +Behind every session-ID, there is a data storage containing all data tags for this session, as long as they are enabled for browser propagation. +These data storages can only be created, by sending requests to the target application, which the Ocelot-agent is attached to. +You cannot create new data storages for example by pushing data into the HTTP-server by using the API. +If a request to the REST-API contains a session-ID, which does not exist in InspectIT, the API will always return 404. + +The HTTP-exporter can only store a specific amount of sessions, which can be configured in the configuration server. +Sessions will be deleted after their _time-to-live_ is expired. + +#### Session limits + +There are some limitations for every session to prevent excessive memory consumption. +The length of the session-id is restricted to a minimum of 16 characters and a maximum of 512 characters. +Furthermore, every session is able to contain up to **128 data keys**. +The maximum length for data keys are **128 chars**. The maximum length for data values are **2048 chars**. + +#### Runtime Updates + +All properties of the HTTP-exporter can be updated during runtime. However, changing properties will result in a server +restart, which also deletes all data currently stored in the server. The following properties are nested properties below the `inspectit.exporters.tags.http` property: -| Property | Default | Description -|------------------|--------------|---| -| `.enabled` | `DISABLED` |If `ENABLED` or `IF_CONFIGURED`, the inspectIT Ocelot agent will try to start the exporter and HTTP server. -| `.host` | `0.0.0.0` |The hostname or network address to which the HTTP server should bind. -| `.port` | `9000` |The port the HTTP server should use. -| `.path` | `/inspectit` |The path on which the HTTP endpoints will be available. -| `.session-limit` | `100` |How many sessions can be stored in the server at the same time. -| `.time-to-live` | `300` |How long sessions should be stored in the server in seconds. +| Property | Default | Description +|----------------------|--------------|---| +| `.enabled` | `DISABLED` |If `ENABLED` or `IF_CONFIGURED`, the inspectIT Ocelot agent will try to start the exporter and HTTP server. +| `.host` | `127.0.0.1` |The hostname or network address to which the HTTP server should bind. +| `.port` | `9000` |The port the HTTP server should use. +| `.path` | `/inspectit` |The path on which the HTTP endpoints will be available. +| `.allowed-origins` | `["*"]` |A list of allowed origins, which are able to access the http-server. +| `.session-limit` | `100` |How many sessions can be stored in the server at the same time. +| `.session-id-header` | `Cookie` |The header, which will be read during propagation to extract the session-ID from +| `.time-to-live` | `300` |How long sessions should be stored in the server in seconds. + +The data of the HTTP exporter is stored inside internal data storages. Data tags will only be written to the storage, +if they are enabled for [browser propagation](../instrumentation/rules.md#data-propagation). + +### Client Example + +This example should demonstrate, how you can call the REST-API in your frontend application. + +```javascript +// Send some requests to transfer the session-id to inspectIT +callBackend(); + +// Send GET-request +getTags(); + +// Send PUT-request +putTags(); + + +function getTags() { + const xhr = new XMLHttpRequest(); + const url = "http://localhost:9000/inspectit"; + + xhr.open("GET", url); + xhr.withCredentials = true; // Send cookies with the request + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + let receivedData = xhr.responseText; // Read received data + console.info(receivedData); + } else { + console.error("Error fetching data: ", xhr.status); + } + } + }; + xhr.send(); +} + +function putTags() { + const data = [ + {"service": "test-01"}, {"url": "www.example.com"} + ] + const xhr = new XMLHttpRequest(); + const url = "http://localhost:9000/inspectit"; + + xhr.open("PUT", url); + xhr.withCredentials = true; // Send cookies with the request + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + console.log("Data fetched successfully!"); + } else { + console.error("Error fetching data: ", xhr.status); + } + } + }; + xhr.send(JSON.stringify(data)); +} +``` + +### OpenAPI documentation + +Below you can see the OpenAPI documentation for the REST-API in YAML-format: + +```yaml +openapi: 3.0.0 +info: + title: Tags Http Exporter + description: | + The API provides access to data tags, which are stored on the Tags HTTP-server of a InspectIT-java-agent. One data tag consists of a key-value-pair. + Data Tags will be stored in the server, if they are enabled for browser-propagation in the InspectIT configuration server. + Using the API, those tags can be read, written and overwritten. However, every data tag will be stored behind a session-ID by InspectIT. + + In order to use a session-ID for storing data tags, a request has to been sent to the target application, which the InspectIT-java-agent is attached to. + This request should contain the session-ID in it's headers. + All data tags created by this request, will be stored behind the provided session-ID as long as they are enabled for browser-propagation. + + To access specific data tags on the server via this API, requests will also need to contain the corresponding session-ID inside their header. + Which header should be used to read the session-ID, can be configured in the InspectIT configuration server. Per default, the Cookie-header will be used. + Data tags are only cached for a specific amount of time, which is also defined in the InspectIT configuration server. + contact: + name: Novatec-Consulting GmbH + url: https://inspectit.rocks/ + email: vhv-team@novatec-gmbh.de + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 + version: 1.0.0 +servers: + - url: http://localhost:9000/inspectit + description: default-URL of the API. However, the host, port and path can be configured in the InspectIT configuration-server +paths: + /inspectit: + summary: Single path of the API + description: Single path of the API, which can be configured in the InspectIT configuration-server + get: + summary: Read currently stored data tags + description: | + Provides all currently stored data tags for the specified session in the session-ID. data tags will be returend as a set of map-entries. + Data tags will only be stored in the tags-server, if they are enabled for browser-propagation. + + Note that all data tags are only cached for a specific amount of time, which can be configured in the InspectIT configuration server. + security: + - session_id: [] + responses: + '200': + description: Success - Response contains all current data tags + content: + application/json: + schema: + type: array + items: + type: object + additionalProperties: + type: string + example: '[{"key1": "value1"}, {"key2": "value2"}]' + '400': + description: Failure - Missing session-ID-header + '403': + description: Forbidden - Not allowed Origin header + '404': + description: Failure - Session-ID not found in session-ID-header + put: + summary: Write or overwrite data tags + description: | + Overwrites data tags that are already stored in the Tags HTTP-server for the specified session-ID. + Alternatively, write new data tags into the storage, to allow the InspectIT-java-agent to use tags, which are not available inside the JVM. + However, new data tags can only be written, if there already exists a data tag storage for the provided session-ID. + It is not possible to create new data tag storages through this API, but only within the InspectIT-java-agent, by sending request to the target application. -The data of the HTTP exporter is stored inside an internal data storage. Data can be written to the storage -by using [browser propagation](../instrumentation/rules.md#data-propagation) + Note that these new data tags also have to be enabled for browser-propagation as well as down-propagation in the InspectIT configuration server. + security: + - session_id: [] + requestBody: + description: data tags should be written or overwritten + required: true + content: + application/json: + schema: + type: array + items: + type: object + additionalProperties: + type: string + example: '[{"key1": "value1"}, {"key2": "value2"}]' + responses: + '200': + description: Success - Data tags have been written + '400': + description: Failure - Invalid request body or missing session-ID-header + '403': + description: Forbidden - Not allowed Origin header + '404': + description: Failure - Session-ID not found in session-ID-header + options: + summary: Cross-Origin safety check + description: | + Allows to send pre-flight-requests to the API before actual requests. The usage is voluntary. + parameters: + - name: Origin + in: header + required: true + schema: + type: string + - name: Access-Control-Request-Method + in: header + required: true + schema: + type: string + responses: + '200': + description: Success - Pre-flight response with CORS headers + headers: + Access-Control-Allow-Origin: + schema: + type: string + Access-Control-Allow-Methods: + schema: + type: string + Access-Control-Allow-Credentials: + schema: + type: boolean + '403': + description: Forbidden - Missing required headers +components: + securitySchemes: + session_id: + type: apiKey + description: | + ID of the current session. The session-ID-header-name can be configured in the InspectIT configuration-server. + Per default, the Cookie-Header will be used as the session-ID-header. + The length of the session-id is restricted to a minimum of 16 characters and a maximum of 512 characters. + name: Cookie + in: header +externalDocs: + description: More information about the Exporter API + url: https://inspectit.github.io/inspectit-ocelot/docs/tags/tags-exporters +```