Skip to content

Commit

Permalink
feat(graphql): implement features to support dashboard automated anal…
Browse files Browse the repository at this point in the history
…ysis card (#312)

* feat(graphql): implement features to support dashboard automated analysis card

* disable JVM ID node filtering, populate archived recordings query with data

* populate aggregate size

* refactor

* refactor to split out class
  • Loading branch information
andrewazores committed Mar 11, 2024
1 parent 6c2f6e0 commit cead773
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 207 deletions.
177 changes: 177 additions & 0 deletions src/main/java/io/cryostat/graphql/ActiveRecordings.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* 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 io.cryostat.graphql;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import org.openjdk.jmc.common.unit.QuantityConversionException;

import io.cryostat.core.templates.Template;
import io.cryostat.core.templates.TemplateType;
import io.cryostat.graphql.TargetNodes.AggregateInfo;
import io.cryostat.graphql.TargetNodes.Recordings;
import io.cryostat.graphql.matchers.LabelSelectorMatcher;
import io.cryostat.recordings.ActiveRecording;
import io.cryostat.recordings.RecordingHelper;
import io.cryostat.recordings.RecordingHelper.RecordingOptions;
import io.cryostat.recordings.RecordingHelper.RecordingReplace;
import io.cryostat.recordings.Recordings.Metadata;
import io.cryostat.targets.Target;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.graphql.api.Nullable;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jdk.jfr.RecordingState;
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.NonNull;
import org.eclipse.microprofile.graphql.Source;

@GraphQLApi
public class ActiveRecordings {

@Inject RecordingHelper recordingHelper;

@Blocking
@Transactional
@Description("Start a new Flight Recording on the specified Target")
public Uni<ActiveRecording> doStartRecording(
@Source Target target, @NonNull RecordingSettings settings)
throws QuantityConversionException {
var fTarget = Target.<Target>findById(target.id);
Template template =
recordingHelper.getPreferredTemplate(
fTarget, settings.template, settings.templateType);
return recordingHelper.startRecording(
fTarget,
RecordingReplace.STOPPED,
template,
settings.asOptions(),
settings.metadata.labels());
}

@Blocking
@Transactional
@Description("Create a new Flight Recorder Snapshot on the specified Target")
public Uni<ActiveRecording> doSnapshot(@Source Target target) {
var fTarget = Target.<Target>findById(target.id);
return recordingHelper.createSnapshot(fTarget);
}

public TargetNodes.ActiveRecordings active(
@Source Recordings recordings, ActiveRecordingsFilter filter) {
var out = new TargetNodes.ActiveRecordings();
out.data = new ArrayList<>();
out.aggregate = AggregateInfo.empty();

var in = recordings.active;
if (in != null && in.data != null) {
out.data =
in.data.stream().filter(r -> filter == null ? true : filter.test(r)).toList();
out.aggregate = AggregateInfo.fromActive(out.data);
}

return out;
}

@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public static class RecordingSettings {
public @NonNull String name;
public @NonNull String template;
public @NonNull TemplateType templateType;
public @Nullable RecordingReplace replace;
public @Nullable Boolean continuous;
public @Nullable Boolean archiveOnStop;
public @Nullable Boolean toDisk;
public @Nullable Long duration;
public @Nullable Long maxSize;
public @Nullable Long maxAge;
public @Nullable Metadata metadata;

public RecordingOptions asOptions() {
return new RecordingOptions(
name,
Optional.ofNullable(toDisk),
Optional.ofNullable(archiveOnStop),
Optional.ofNullable(duration),
Optional.ofNullable(maxSize),
Optional.ofNullable(maxAge));
}
}

@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public static class ActiveRecordingsFilter implements Predicate<ActiveRecording> {
public @Nullable String name;
public @Nullable List<String> names;
public @Nullable List<String> labels;
public @Nullable RecordingState state;
public @Nullable Boolean continuous;
public @Nullable Boolean toDisk;
public @Nullable Long durationMsGreaterThanEqual;
public @Nullable Long durationMsLessThanEqual;
public @Nullable Long startTimeMsAfterEqual;
public @Nullable Long startTimeMsBeforeEqual;

@Override
public boolean test(ActiveRecording r) {
Predicate<ActiveRecording> matchesName =
n -> name == null || Objects.equals(name, n.name);
Predicate<ActiveRecording> matchesNames = n -> names == null || names.contains(n.name);
Predicate<ActiveRecording> matchesLabels =
n ->
labels == null
|| labels.stream()
.allMatch(
label ->
LabelSelectorMatcher.parse(label)
.test(n.metadata.labels()));
Predicate<ActiveRecording> matchesState = n -> state == null || n.state.equals(state);
Predicate<ActiveRecording> matchesContinuous =
n -> continuous == null || continuous.equals(n.continuous);
Predicate<ActiveRecording> matchesToDisk =
n -> toDisk == null || toDisk.equals(n.toDisk);
Predicate<ActiveRecording> matchesDurationGte =
n ->
durationMsGreaterThanEqual == null
|| durationMsGreaterThanEqual >= n.duration;
Predicate<ActiveRecording> matchesDurationLte =
n -> durationMsLessThanEqual == null || durationMsLessThanEqual <= n.duration;
Predicate<ActiveRecording> matchesStartTimeAfter =
n -> startTimeMsAfterEqual == null || startTimeMsAfterEqual >= n.startTime;
Predicate<ActiveRecording> matchesStartTimeBefore =
n -> startTimeMsBeforeEqual == null || startTimeMsBeforeEqual <= n.startTime;

return matchesName
.and(matchesNames)
.and(matchesLabels)
.and(matchesState)
.and(matchesContinuous)
.and(matchesToDisk)
.and(matchesDurationGte)
.and(matchesDurationLte)
.and(matchesStartTimeBefore)
.and(matchesStartTimeAfter)
.test(r);
}
}
}
123 changes: 123 additions & 0 deletions src/main/java/io/cryostat/graphql/ArchivedRecordings.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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 io.cryostat.graphql;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

import io.cryostat.graphql.TargetNodes.AggregateInfo;
import io.cryostat.graphql.TargetNodes.Recordings;
import io.cryostat.graphql.matchers.LabelSelectorMatcher;
import io.cryostat.recordings.RecordingHelper;
import io.cryostat.recordings.Recordings.ArchivedRecording;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.graphql.api.Nullable;
import jakarta.inject.Inject;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Query;
import org.eclipse.microprofile.graphql.Source;

@GraphQLApi
public class ArchivedRecordings {

@Inject RecordingHelper recordingHelper;

@Blocking
@Query("archivedRecordings")
public TargetNodes.ArchivedRecordings listArchivedRecordings(ArchivedRecordingsFilter filter) {
var r = new TargetNodes.ArchivedRecordings();
r.data = recordingHelper.listArchivedRecordings();
r.aggregate = AggregateInfo.fromArchived(r.data);
r.aggregate.size = r.data.stream().mapToLong(ArchivedRecording::size).sum();
r.aggregate.count = r.data.size();
return r;
}

public TargetNodes.ArchivedRecordings archived(
@Source Recordings recordings, ArchivedRecordingsFilter filter) {
var out = new TargetNodes.ArchivedRecordings();
out.data = new ArrayList<>();
out.aggregate = AggregateInfo.empty();

var in = recordings.archived;
if (in != null && in.data != null) {
out.data =
in.data.stream().filter(r -> filter == null ? true : filter.test(r)).toList();
out.aggregate = AggregateInfo.fromArchived(out.data);
}

return out;
}

@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public static class ArchivedRecordingsFilter implements Predicate<ArchivedRecording> {
public @Nullable String name;
public @Nullable List<String> names;
public @Nullable String sourceTarget;
public @Nullable List<String> labels;
public @Nullable Long sizeBytesGreaterThanEqual;
public @Nullable Long sizeBytesLessThanEqual;
public @Nullable Long archivedTimeAfterEqual;
public @Nullable Long archivedTimeBeforeEqual;

@Override
public boolean test(ArchivedRecording r) {
Predicate<ArchivedRecording> matchesName =
n -> name == null || Objects.equals(name, n.name());
Predicate<ArchivedRecording> matchesNames =
n -> names == null || names.contains(n.name());
Predicate<ArchivedRecording> matchesSourceTarget =
n ->
sourceTarget == null
|| Objects.equals(
r.metadata().labels().get("connectUrl"), sourceTarget);
Predicate<ArchivedRecording> matchesLabels =
n ->
labels == null
|| labels.stream()
.allMatch(
label ->
LabelSelectorMatcher.parse(label)
.test(n.metadata().labels()));
Predicate<ArchivedRecording> matchesSizeGte =
n -> sizeBytesGreaterThanEqual == null || sizeBytesGreaterThanEqual >= n.size();
Predicate<ArchivedRecording> matchesSizeLte =
n -> sizeBytesLessThanEqual == null || sizeBytesLessThanEqual <= n.size();
Predicate<ArchivedRecording> matchesArchivedTimeGte =
n ->
archivedTimeAfterEqual == null
|| archivedTimeAfterEqual >= n.archivedTime();
Predicate<ArchivedRecording> matchesArchivedTimeLte =
n ->
archivedTimeBeforeEqual == null
|| archivedTimeBeforeEqual <= n.archivedTime();

return matchesName
.and(matchesNames)
.and(matchesSourceTarget)
.and(matchesLabels)
.and(matchesSizeGte)
.and(matchesSizeLte)
.and(matchesArchivedTimeGte)
.and(matchesArchivedTimeLte)
.test(r);
}
}
}
55 changes: 55 additions & 0 deletions src/main/java/io/cryostat/graphql/RecordingLinks.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 io.cryostat.graphql;

import io.cryostat.recordings.ActiveRecording;
import io.cryostat.recordings.RecordingHelper;
import io.cryostat.recordings.Recordings.ArchivedRecording;

import jakarta.inject.Inject;
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Source;

@GraphQLApi
public class RecordingLinks {

@Inject RecordingHelper recordingHelper;

@Description("URL for GET request to retrieve the JFR binary file content of this recording")
public String downloadUrl(@Source ActiveRecording recording) {
return recordingHelper.downloadUrl(recording);
}

@Description(
"URL for GET request to retrieve a JSON formatted Automated Analysis Report of this"
+ " recording")
public String reportUrl(@Source ActiveRecording recording) {
return recordingHelper.reportUrl(recording);
}

@Description("URL for GET request to retrieve the JFR binary file content of this recording")
public String downloadUrl(@Source ArchivedRecording recording) {
return recording.downloadUrl();
}

@Description(
"URL for GET request to retrieve a JSON formatted Automated Analysis Report of this"
+ " recording")
public String reportUrl(@Source ArchivedRecording recording) {
return recording.reportUrl();
}
}
Loading

0 comments on commit cead773

Please sign in to comment.