From 5a9d4c4604384d5bd1ac4ea92f0a6db8c0f8502c Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 20 Mar 2023 15:16:39 -0400 Subject: [PATCH] feat(api): remote readonly events access (#80) * add event types fetching context Signed-off-by: Andrew Azores * implement event templates query * JDK11 compat fixup * add 'deflate' response body filter Signed-off-by: Andrew Azores * refactor cleanup * fixup! refactor cleanup Signed-off-by: Andrew Azores --------- Signed-off-by: Andrew Azores --- .../java/io/cryostat/agent/MainModule.java | 1 - .../java/io/cryostat/agent/WebServer.java | 54 +++++++ .../agent/remote/EventTemplatesContext.java | 99 +++++++++++++ .../agent/remote/EventTypesContext.java | 139 ++++++++++++++++++ .../cryostat/agent/remote/RemoteModule.java | 8 + 5 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java create mode 100644 src/main/java/io/cryostat/agent/remote/EventTypesContext.java diff --git a/src/main/java/io/cryostat/agent/MainModule.java b/src/main/java/io/cryostat/agent/MainModule.java index 970cb275..bee21184 100644 --- a/src/main/java/io/cryostat/agent/MainModule.java +++ b/src/main/java/io/cryostat/agent/MainModule.java @@ -181,7 +181,6 @@ public static HttpClient provideHttpClient( } @Provides - @Singleton public static ObjectMapper provideObjectMapper() { return new ObjectMapper(); } diff --git a/src/main/java/io/cryostat/agent/WebServer.java b/src/main/java/io/cryostat/agent/WebServer.java index aac9ff25..f10d312e 100644 --- a/src/main/java/io/cryostat/agent/WebServer.java +++ b/src/main/java/io/cryostat/agent/WebServer.java @@ -46,9 +46,12 @@ import java.time.Duration; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import java.util.zip.DeflaterOutputStream; import io.cryostat.agent.remote.RemoteContext; @@ -77,6 +80,7 @@ class WebServer { private final AgentAuthenticator agentAuthenticator; private final RequestLoggingFilter requestLoggingFilter; + private final CompressionFilter compressionFilter; WebServer( Lazy> remoteContexts, @@ -95,6 +99,7 @@ class WebServer { this.agentAuthenticator = new AgentAuthenticator(); this.requestLoggingFilter = new RequestLoggingFilter(); + this.compressionFilter = new CompressionFilter(); } void start() throws IOException, NoSuchAlgorithmException { @@ -114,6 +119,7 @@ void start() throws IOException, NoSuchAlgorithmException { HttpContext ctx = this.http.createContext(rc.path(), rc::handle); ctx.setAuthenticator(agentAuthenticator); ctx.getFilters().add(requestLoggingFilter); + ctx.getFilters().add(compressionFilter); }); this.http.start(); @@ -164,6 +170,54 @@ public String description() { } } + private class CompressionFilter extends Filter { + + @Override + public void doFilter(HttpExchange exchange, Chain chain) throws IOException { + List requestedEncodings = + exchange.getRequestHeaders().getOrDefault("Accept-Encoding", List.of()).stream() + .map(raw -> raw.replaceAll("\\s", "")) + .map(raw -> raw.split(",")) + .map(Arrays::asList) + .flatMap(List::stream) + .collect(Collectors.toList()); + String negotiatedEncoding = null; + priority: + for (String encoding : requestedEncodings) { + switch (encoding) { + case "deflate": + negotiatedEncoding = encoding; + exchange.setStreams( + exchange.getRequestBody(), + new DeflaterOutputStream(exchange.getResponseBody())); + break priority; + // TODO gzip encoding breaks communication with the server, need to + // determine why and re-enable this + // case "gzip": + // actualEncoding = requestedEncoding; + // exchange.setStreams( + // exchange.getRequestBody(), + // new GZIPOutputStream(exchange.getResponseBody())); + // break priority; + default: + break; + } + } + if (negotiatedEncoding == null) { + log.info("Using no encoding"); + } else { + log.info("Using '{}' encoding", negotiatedEncoding); + exchange.getResponseHeaders().put("Content-Encoding", List.of(negotiatedEncoding)); + } + chain.doFilter(exchange); + } + + @Override + public String description() { + return "responseCompression"; + } + } + private class PingContext implements RemoteContext { @Override diff --git a/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java b/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java new file mode 100644 index 00000000..432391c2 --- /dev/null +++ b/src/main/java/io/cryostat/agent/remote/EventTemplatesContext.java @@ -0,0 +1,99 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.agent.remote; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.HttpExchange; +import jdk.management.jfr.ConfigurationInfo; +import jdk.management.jfr.FlightRecorderMXBean; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class EventTemplatesContext implements RemoteContext { + + private final Logger log = LoggerFactory.getLogger(getClass()); + private final ObjectMapper mapper; + + @Inject + EventTemplatesContext(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public String path() { + return "/event-templates"; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "GET": + try { + exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); + try (OutputStream response = exchange.getResponseBody()) { + FlightRecorderMXBean bean = + ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class); + List xmlTexts = + bean.getConfigurations().stream() + .map(ConfigurationInfo::getContents) + .collect(Collectors.toList()); + mapper.writeValue(response, xmlTexts); + } + } catch (Exception e) { + log.error("events serialization failure", e); + } finally { + exchange.close(); + } + break; + default: + exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); + exchange.close(); + break; + } + } +} diff --git a/src/main/java/io/cryostat/agent/remote/EventTypesContext.java b/src/main/java/io/cryostat/agent/remote/EventTypesContext.java new file mode 100644 index 00000000..9e329da1 --- /dev/null +++ b/src/main/java/io/cryostat/agent/remote/EventTypesContext.java @@ -0,0 +1,139 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.agent.remote; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.HttpExchange; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import jdk.jfr.EventType; +import jdk.jfr.FlightRecorder; +import jdk.jfr.SettingDescriptor; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class EventTypesContext implements RemoteContext { + + private final Logger log = LoggerFactory.getLogger(getClass()); + private final ObjectMapper mapper; + + @Inject + EventTypesContext(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public String path() { + return "/event-types"; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "GET": + try { + List events = getEventTypes(); + exchange.sendResponseHeaders(HttpStatus.SC_OK, 0); + try (OutputStream response = exchange.getResponseBody()) { + mapper.writeValue(response, events); + } + } catch (Exception e) { + log.error("events serialization failure", e); + } finally { + exchange.close(); + } + break; + default: + exchange.sendResponseHeaders(HttpStatus.SC_NOT_FOUND, -1); + exchange.close(); + break; + } + } + + private List getEventTypes() { + return FlightRecorder.getFlightRecorder().getEventTypes().stream() + .map( + evt -> { + EventInfo evtInfo = new EventInfo(evt); + evtInfo.settings.addAll( + evt.getSettingDescriptors().stream() + .map(SettingInfo::new) + .collect(Collectors.toList())); + return evtInfo; + }) + .collect(Collectors.toList()); + } + + @SuppressFBWarnings(value = "URF_UNREAD_FIELD") + private static class EventInfo { + + public final String name; + public final String label; + public final String description; + public final List categories; + public final List settings = new ArrayList<>(); + + EventInfo(EventType evt) { + this.name = evt.getName(); + this.label = evt.getLabel(); + this.description = evt.getDescription(); + this.categories = evt.getCategoryNames(); + } + } + + @SuppressFBWarnings(value = "URF_UNREAD_FIELD") + private static class SettingInfo { + + public final String name; + public final String defaultValue; + + SettingInfo(SettingDescriptor desc) { + this.name = desc.getName(); + this.defaultValue = desc.getDefaultValue(); + } + } +} diff --git a/src/main/java/io/cryostat/agent/remote/RemoteModule.java b/src/main/java/io/cryostat/agent/remote/RemoteModule.java index 3d16ffd2..3b37cdff 100644 --- a/src/main/java/io/cryostat/agent/remote/RemoteModule.java +++ b/src/main/java/io/cryostat/agent/remote/RemoteModule.java @@ -47,4 +47,12 @@ public abstract class RemoteModule { @Binds @IntoSet abstract RemoteContext bindMBeanContext(MBeanContext ctx); + + @Binds + @IntoSet + abstract RemoteContext bindEventTypesContext(EventTypesContext ctx); + + @Binds + @IntoSet + abstract RemoteContext bindEventTemplatesContext(EventTemplatesContext ctx); }