From 29b9c52e04df8f1b163b452acb4c1d12d4d773d1 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 4 Mar 2024 13:00:03 -0500 Subject: [PATCH] feat(graphql): implement features to support dashboard automated analysis 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 --- .../io/cryostat/graphql/ActiveRecordings.java | 177 ++++++++++++++ .../cryostat/graphql/ArchivedRecordings.java | 123 ++++++++++ .../io/cryostat/graphql/RecordingLinks.java | 55 +++++ .../java/io/cryostat/graphql/TargetNodes.java | 229 ++---------------- 4 files changed, 377 insertions(+), 207 deletions(-) create mode 100644 src/main/java/io/cryostat/graphql/ActiveRecordings.java create mode 100644 src/main/java/io/cryostat/graphql/ArchivedRecordings.java create mode 100644 src/main/java/io/cryostat/graphql/RecordingLinks.java diff --git a/src/main/java/io/cryostat/graphql/ActiveRecordings.java b/src/main/java/io/cryostat/graphql/ActiveRecordings.java new file mode 100644 index 000000000..ff011c236 --- /dev/null +++ b/src/main/java/io/cryostat/graphql/ActiveRecordings.java @@ -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 doStartRecording( + @Source Target target, @NonNull RecordingSettings settings) + throws QuantityConversionException { + var fTarget = 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 doSnapshot(@Source Target target) { + var fTarget = 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 { + public @Nullable String name; + public @Nullable List names; + public @Nullable List 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 matchesName = + n -> name == null || Objects.equals(name, n.name); + Predicate matchesNames = n -> names == null || names.contains(n.name); + Predicate matchesLabels = + n -> + labels == null + || labels.stream() + .allMatch( + label -> + LabelSelectorMatcher.parse(label) + .test(n.metadata.labels())); + Predicate matchesState = n -> state == null || n.state.equals(state); + Predicate matchesContinuous = + n -> continuous == null || continuous.equals(n.continuous); + Predicate matchesToDisk = + n -> toDisk == null || toDisk.equals(n.toDisk); + Predicate matchesDurationGte = + n -> + durationMsGreaterThanEqual == null + || durationMsGreaterThanEqual >= n.duration; + Predicate matchesDurationLte = + n -> durationMsLessThanEqual == null || durationMsLessThanEqual <= n.duration; + Predicate matchesStartTimeAfter = + n -> startTimeMsAfterEqual == null || startTimeMsAfterEqual >= n.startTime; + Predicate 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); + } + } +} diff --git a/src/main/java/io/cryostat/graphql/ArchivedRecordings.java b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java new file mode 100644 index 000000000..eec113d24 --- /dev/null +++ b/src/main/java/io/cryostat/graphql/ArchivedRecordings.java @@ -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 { + public @Nullable String name; + public @Nullable List names; + public @Nullable String sourceTarget; + public @Nullable List labels; + public @Nullable Long sizeBytesGreaterThanEqual; + public @Nullable Long sizeBytesLessThanEqual; + public @Nullable Long archivedTimeAfterEqual; + public @Nullable Long archivedTimeBeforeEqual; + + @Override + public boolean test(ArchivedRecording r) { + Predicate matchesName = + n -> name == null || Objects.equals(name, n.name()); + Predicate matchesNames = + n -> names == null || names.contains(n.name()); + Predicate matchesSourceTarget = + n -> + sourceTarget == null + || Objects.equals( + r.metadata().labels().get("connectUrl"), sourceTarget); + Predicate matchesLabels = + n -> + labels == null + || labels.stream() + .allMatch( + label -> + LabelSelectorMatcher.parse(label) + .test(n.metadata().labels())); + Predicate matchesSizeGte = + n -> sizeBytesGreaterThanEqual == null || sizeBytesGreaterThanEqual >= n.size(); + Predicate matchesSizeLte = + n -> sizeBytesLessThanEqual == null || sizeBytesLessThanEqual <= n.size(); + Predicate matchesArchivedTimeGte = + n -> + archivedTimeAfterEqual == null + || archivedTimeAfterEqual >= n.archivedTime(); + Predicate 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); + } + } +} diff --git a/src/main/java/io/cryostat/graphql/RecordingLinks.java b/src/main/java/io/cryostat/graphql/RecordingLinks.java new file mode 100644 index 000000000..fbc732d55 --- /dev/null +++ b/src/main/java/io/cryostat/graphql/RecordingLinks.java @@ -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(); + } +} diff --git a/src/main/java/io/cryostat/graphql/TargetNodes.java b/src/main/java/io/cryostat/graphql/TargetNodes.java index 5ea0e203a..5648c3b22 100644 --- a/src/main/java/io/cryostat/graphql/TargetNodes.java +++ b/src/main/java/io/cryostat/graphql/TargetNodes.java @@ -15,31 +15,16 @@ */ package io.cryostat.graphql; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; - -import org.openjdk.jmc.common.unit.QuantityConversionException; import io.cryostat.core.net.JFRConnection; import io.cryostat.core.net.MBeanMetrics; -import io.cryostat.core.templates.Template; -import io.cryostat.core.templates.TemplateType; import io.cryostat.discovery.DiscoveryNode; import io.cryostat.graphql.RootNode.DiscoveryNodeFilter; -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.ArchivedRecording; -import io.cryostat.recordings.Recordings.Metadata; import io.cryostat.targets.Target; import io.cryostat.targets.TargetConnectionManager; @@ -50,11 +35,9 @@ import graphql.schema.GraphQLSchema; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.api.Context; -import io.smallrye.graphql.api.Nullable; import io.smallrye.mutiny.Uni; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; -import jakarta.transaction.Transactional; import jdk.jfr.RecordingState; import org.eclipse.microprofile.graphql.Description; import org.eclipse.microprofile.graphql.GraphQLApi; @@ -101,16 +84,19 @@ public List getTargetNodes(DiscoveryNodeFilter filter) { // load the entire discovery tree out of the database, then perform the filtering at the // application level. return Target.findAll().stream() - .filter(distinctWith(t -> t.jvmId)) + // FIXME filtering by distinct JVM ID breaks clients that expect to be able to use a + // different connection URL (in the node filter or for client-side filtering) than + // the one we end up selecting for here. + // .filter(distinctWith(t -> t.jvmId)) .map(t -> t.discoveryNode) .filter(n -> filter == null ? true : filter.test(n)) .toList(); } - private static Predicate distinctWith(Function fn) { - Set observed = ConcurrentHashMap.newKeySet(); - return t -> observed.add(fn.apply(t)); - } + // private static Predicate distinctWith(Function fn) { + // Set observed = ConcurrentHashMap.newKeySet(); + // return t -> observed.add(fn.apply(t)); + // } @Blocking @Description("Get the active and archived recordings belonging to this target") @@ -124,15 +110,13 @@ public Recordings recordings(@Source Target target, Context context) { if (requestedFields.contains("active")) { recordings.active = new ActiveRecordings(); recordings.active.data = target.activeRecordings; - recordings.active.aggregate = new AggregateInfo(); - recordings.active.aggregate.count = recordings.active.data.size(); - recordings.active.aggregate.size = 0; + recordings.active.aggregate = AggregateInfo.fromActive(recordings.active.data); } if (requestedFields.contains("archived")) { recordings.archived = new ArchivedRecordings(); recordings.archived.data = recordingHelper.listArchivedRecordings(target); - recordings.archived.aggregate = new AggregateInfo(); + recordings.archived.aggregate = AggregateInfo.fromArchived(recordings.archived.data); recordings.archived.aggregate.count = recordings.archived.data.size(); recordings.archived.aggregate.size = recordings.archived.data.stream().mapToLong(ArchivedRecording::size).sum(); @@ -141,71 +125,12 @@ public Recordings recordings(@Source Target target, Context context) { return recordings; } - public ActiveRecordings active(@Source Recordings recordings, ActiveRecordingsFilter filter) { - var out = new ActiveRecordings(); - out.data = new ArrayList<>(); - out.aggregate = new AggregateInfo(); - - 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.size = 0; - out.aggregate.count = out.data.size(); - } - - return out; - } - - public ArchivedRecordings archived( - @Source Recordings recordings, ArchivedRecordingsFilter filter) { - var out = new ArchivedRecordings(); - out.data = new ArrayList<>(); - out.aggregate = new AggregateInfo(); - - 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.size = 0; - out.aggregate.count = out.data.size(); - } - - return out; - } - @Blocking @Description("Get live MBean metrics snapshot from the specified Target") public Uni mbeanMetrics(@Source Target target) { return connectionManager.executeConnectedTaskUni(target, JFRConnection::getMBeanMetrics); } - @Blocking - @Transactional - @Description("Start a new Flight Recording on the specified Target") - public Uni doStartRecording( - @Source Target target, @NonNull RecordingSettings settings) - throws QuantityConversionException { - var fTarget = 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 doSnapshot(@Source Target target) { - var fTarget = Target.findById(target.id); - return recordingHelper.createSnapshot(fTarget); - } - @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public static class Recordings { public @NonNull ActiveRecordings active; @@ -230,134 +155,24 @@ public static class AggregateInfo { public @NonNull @Description( "The sum of sizes of elements in this collection, or 0 if not applicable") long size; - } - - @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") - public static class ActiveRecordingsFilter implements Predicate { - public @Nullable String name; - public @Nullable List names; - public @Nullable List 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 matchesName = - n -> name == null || Objects.equals(name, n.name); - Predicate matchesNames = n -> names == null || names.contains(n.name); - Predicate matchesLabels = - n -> - labels == null - || labels.stream() - .allMatch( - label -> - LabelSelectorMatcher.parse(label) - .test(n.metadata.labels())); - Predicate matchesState = n -> state == null || n.state.equals(state); - Predicate matchesContinuous = - n -> continuous == null || continuous.equals(n.continuous); - Predicate matchesToDisk = - n -> toDisk == null || toDisk.equals(n.toDisk); - Predicate matchesDurationGte = - n -> - durationMsGreaterThanEqual == null - || durationMsGreaterThanEqual >= n.duration; - Predicate matchesDurationLte = - n -> durationMsLessThanEqual == null || durationMsLessThanEqual <= n.duration; - Predicate matchesStartTimeAfter = - n -> startTimeMsAfterEqual == null || startTimeMsAfterEqual >= n.startTime; - Predicate 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); + private AggregateInfo(long count, long size) { + this.count = count; + this.size = size; } - } - - @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") - public static class ArchivedRecordingsFilter implements Predicate { - public @Nullable String name; - public @Nullable List names; - public @Nullable List labels; - public @Nullable Long sizeBytesGreaterThanEqual; - public @Nullable Long sizeBytesLessThanEqual; - public @Nullable Long archivedTimeAfterEqual; - public @Nullable Long archivedTimeBeforeEqual; - - @Override - public boolean test(ArchivedRecording r) { - Predicate matchesName = - n -> name == null || Objects.equals(name, n.name()); - Predicate matchesNames = - n -> names == null || names.contains(n.name()); - Predicate matchesLabels = - n -> - labels == null - || labels.stream() - .allMatch( - label -> - LabelSelectorMatcher.parse(label) - .test(n.metadata().labels())); - Predicate matchesSizeGte = - n -> sizeBytesGreaterThanEqual == null || sizeBytesGreaterThanEqual >= n.size(); - Predicate matchesSizeLte = - n -> sizeBytesLessThanEqual == null || sizeBytesLessThanEqual <= n.size(); - Predicate matchesArchivedTimeGte = - n -> - archivedTimeAfterEqual == null - || archivedTimeAfterEqual >= n.archivedTime(); - Predicate matchesArchivedTimeLte = - n -> - archivedTimeBeforeEqual == null - || archivedTimeBeforeEqual <= n.archivedTime(); - return matchesName - .and(matchesNames) - .and(matchesLabels) - .and(matchesSizeGte) - .and(matchesSizeLte) - .and(matchesArchivedTimeGte) - .and(matchesArchivedTimeLte) - .test(r); + public static AggregateInfo empty() { + return new AggregateInfo(0, 0); } - } - @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 static AggregateInfo fromActive(List recordings) { + return new AggregateInfo(recordings.size(), 0); + } - public RecordingOptions asOptions() { - return new RecordingOptions( - name, - Optional.ofNullable(toDisk), - Optional.ofNullable(archiveOnStop), - Optional.ofNullable(duration), - Optional.ofNullable(maxSize), - Optional.ofNullable(maxAge)); + public static AggregateInfo fromArchived(List recordings) { + return new AggregateInfo( + recordings.size(), + recordings.stream().mapToLong(ArchivedRecording::size).sum()); } } }