Skip to content

Commit

Permalink
feat(rules): implement declarative Automated Rules (#749)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewazores authored Jan 13, 2025
1 parent 255bed3 commit 3c81e89
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/main/docker/Dockerfile.jvm
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ ENTRYPOINT [ "/deployments/app/entrypoint.bash", "/opt/jboss/container/java/run/
# We make distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 src/main/docker/include/cryostat.jfc /usr/lib/jvm/jre/lib/jfr/
COPY --chown=185 src/main/docker/include/template_presets/* /opt/cryostat.d/presets.d/
COPY --chown=185 src/main/docker/include/rule_presets/* /opt/cryostat.d/rules.d/
COPY --chown=185 src/main/docker/include/genpass.bash /deployments/app/
COPY --chown=185 src/main/docker/include/entrypoint.bash /deployments/app/
COPY --chown=185 src/main/docker/include/truststore-setup.bash /deployments/app/
Expand Down
7 changes: 7 additions & 0 deletions src/main/docker/include/rule_presets/quarkus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "quarkus",
"description": "Preset Automated Rule for enabling Quarkus framework-specific events when available",
"eventSpecifier": "template=Quarkus,type=PRESET",
"matchExpression": "jfrEventTypeIds(target).exists(x, x.startsWith(\"quarkus\"))",
"enabled": false
}
1 change: 1 addition & 0 deletions src/main/java/io/cryostat/ConfigProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class ConfigProperties {
public static final String CUSTOM_TEMPLATES_DIR = "templates-dir";
public static final String PRESET_TEMPLATES_DIR = "preset-templates-dir";
public static final String SSL_TRUSTSTORE_DIR = "ssl.truststore.dir";
public static final String RULES_DIR = "rules-dir";

public static final String URI_RANGE = "cryostat.target.uri-range";

Expand Down
60 changes: 60 additions & 0 deletions src/main/java/io/cryostat/rules/Rules.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@
*/
package io.cryostat.rules;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;

import io.cryostat.ConfigProperties;
import io.cryostat.expressions.MatchExpression;
import io.cryostat.util.EntityExistsException;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.runtime.StartupEvent;
import io.vertx.core.json.JsonObject;
import io.vertx.mutiny.core.eventbus.EventBus;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
Expand All @@ -36,6 +43,8 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
Expand All @@ -45,7 +54,58 @@
@Path("/api/v4/rules")
public class Rules {

@ConfigProperty(name = ConfigProperties.RULES_DIR)
java.nio.file.Path dir;

@Inject Logger logger;
@Inject EventBus bus;
@Inject ObjectMapper mapper;

@Transactional
void onStart(@Observes StartupEvent evt) {
if (!checkDir()) {
return;
}
try {
Files.walk(dir)
.filter(Files::isRegularFile)
.filter(Files::isReadable)
.forEach(this::processDeclarativeRule);
} catch (IOException e) {
logger.error(e);
}
}

private void processDeclarativeRule(java.nio.file.Path path) {
try (var is = new BufferedInputStream(Files.newInputStream(path))) {
var declarativeRule = mapper.readValue(is, Rule.class);
logger.tracev(
"Processing declarative Automated Rule with name \"{}\" at {}",
declarativeRule.name,
path);
var exists = Rule.find("name", declarativeRule.name).count() != 0;
if (exists) {
logger.tracev(
"Rule with name \"{}\" already exists in database. Skipping declarative"
+ " rule at {}",
declarativeRule.name,
path);
return;
}
declarativeRule.persist();
} catch (IOException ioe) {
logger.warn(ioe);
} catch (Exception e) {
logger.error(e);
}
}

private boolean checkDir() {
return Files.exists(dir)
&& Files.isReadable(dir)
&& Files.isExecutable(dir)
&& Files.isDirectory(dir);
}

@GET
@RolesAllowed("read")
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ cryostat.target.uri-range=PUBLIC
cryostat.agent.tls.required=true

conf-dir=/opt/cryostat.d
rules-dir=${conf-dir}/rules.d
templates-dir=${conf-dir}/templates.d
preset-templates-dir=${conf-dir}/presets.d
ssl.truststore=${conf-dir}/truststore.p12
Expand Down
77 changes: 77 additions & 0 deletions src/test/java/itest/PresetRulesIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package itest;

import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.web.client.HttpRequest;
import itest.bases.StandardSelfTest;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

@QuarkusIntegrationTest
public class PresetRulesIT extends StandardSelfTest {

@Test
public void shouldListPresetRules() throws Exception {
CompletableFuture<JsonArray> future = new CompletableFuture<>();
HttpRequest<Buffer> req = webClient.get("/api/v4/rules");
req.send(
ar -> {
if (ar.failed()) {
future.completeExceptionally(ar.cause());
return;
}
future.complete(ar.result().bodyAsJsonArray());
});
JsonArray response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
MatcherAssert.assertThat(response.size(), Matchers.equalTo(1));
}

@Test
public void shouldHavePresetQuarkusRule() throws Exception {
String url = "/api/v4/rules/quarkus";
File file =
downloadFile(url, "quarkus", ".json")
.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.toFile();

ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree(file);

MatcherAssert.assertThat(json.get("name").asText(), Matchers.equalTo("quarkus"));
MatcherAssert.assertThat(
json.get("description").asText(),
Matchers.equalTo(
"Preset Automated Rule for enabling Quarkus framework-specific events when"
+ " available"));
MatcherAssert.assertThat(
json.get("eventSpecifier").asText(),
Matchers.equalTo("template=Quarkus,type=PRESET"));
MatcherAssert.assertThat(
json.get("matchExpression").asText(),
Matchers.equalTo("jfrEventTypeIds(target).exists(x, x.startsWith(\"quarkus\"))"));
MatcherAssert.assertThat(json.get("enabled").asBoolean(), Matchers.is(false));
}
}
3 changes: 1 addition & 2 deletions src/test/java/itest/PresetTemplatesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public void shouldListPresetTemplates() throws Exception {

@Test
public void shouldHavePresetQuarkusTemplate() throws Exception {
String url =
String.format("/api/v4/event_templates/PRESET/Quarkus", getSelfReferenceTargetId());
String url = "/api/v4/event_templates/PRESET/Quarkus";
File file =
downloadFile(url, "quarkus", ".jfc")
.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)
Expand Down

0 comments on commit 3c81e89

Please sign in to comment.