Skip to content

Commit f309b94

Browse files
authored
New rule definitions trigger on existing targets (#538)
* Refactor to extract activation method * Refactor to extract deactivation method * Add deactivation log statements mirror activation logging * Minor refactor * Partial refactor: don't use webClient when activating rules * Refactor to extract recording creation helper * Apply new rules to existing targets * Refactor TargetRecordingsPostHandler to use RecordingCreationHelper * Complete and enable two RuleProcessorTests * Remove FIXME
1 parent 8245eb7 commit f309b94

File tree

10 files changed

+556
-442
lines changed

10 files changed

+556
-442
lines changed

src/main/java/io/cryostat/MainModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import io.cryostat.net.NetworkModule;
5656
import io.cryostat.net.web.http.HttpMimeType;
5757
import io.cryostat.platform.PlatformModule;
58+
import io.cryostat.recordings.RecordingsModule;
5859
import io.cryostat.rules.Rule;
5960
import io.cryostat.rules.RulesModule;
6061
import io.cryostat.sys.SystemModule;
@@ -80,6 +81,7 @@
8081
CommandsModule.class,
8182
TemplatesModule.class,
8283
RulesModule.class,
84+
RecordingsModule.class,
8385
})
8486
public abstract class MainModule {
8587
public static final String RECORDINGS_PATH = "RECORDINGS_PATH";

src/main/java/io/cryostat/net/ConnectionDescriptor.java

+9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Optional;
4141

4242
import io.cryostat.core.net.Credentials;
43+
import io.cryostat.platform.ServiceRef;
4344

4445
import org.apache.commons.lang3.builder.EqualsBuilder;
4546
import org.apache.commons.lang3.builder.HashCodeBuilder;
@@ -49,10 +50,18 @@ public class ConnectionDescriptor {
4950
private final String targetId;
5051
private final Optional<Credentials> credentials;
5152

53+
public ConnectionDescriptor(ServiceRef serviceRef) {
54+
this(serviceRef.getServiceUri().toString());
55+
}
56+
5257
public ConnectionDescriptor(String targetId) {
5358
this(targetId, null);
5459
}
5560

61+
public ConnectionDescriptor(ServiceRef serviceRef, Credentials credentials) {
62+
this(serviceRef.getServiceUri().toString(), credentials);
63+
}
64+
5665
public ConnectionDescriptor(String targetId, Credentials credentials) {
5766
this.targetId = targetId;
5867
this.credentials = Optional.ofNullable(credentials);

src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java

+36-140
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939

4040
import java.io.IOException;
4141
import java.net.URISyntaxException;
42-
import java.util.Map;
4342
import java.util.Optional;
4443
import java.util.concurrent.TimeUnit;
4544
import java.util.regex.Matcher;
@@ -48,26 +47,22 @@
4847
import javax.inject.Inject;
4948
import javax.inject.Provider;
5049

51-
import org.openjdk.jmc.common.unit.IConstrainedMap;
5250
import org.openjdk.jmc.common.unit.QuantityConversionException;
53-
import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
5451
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
55-
import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
5652
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
5753

58-
import io.cryostat.commands.internal.EventOptionsBuilder;
5954
import io.cryostat.commands.internal.RecordingOptionsBuilderFactory;
6055
import io.cryostat.core.net.JFRConnection;
61-
import io.cryostat.core.templates.Template;
6256
import io.cryostat.core.templates.TemplateType;
6357
import io.cryostat.jmc.serialization.HyperlinkedSerializableRecordingDescriptor;
64-
import io.cryostat.messaging.notifications.NotificationFactory;
6558
import io.cryostat.net.AuthManager;
59+
import io.cryostat.net.ConnectionDescriptor;
6660
import io.cryostat.net.TargetConnectionManager;
6761
import io.cryostat.net.web.WebServer;
6862
import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
6963
import io.cryostat.net.web.http.HttpMimeType;
7064
import io.cryostat.net.web.http.api.ApiVersion;
65+
import io.cryostat.recordings.RecordingCreationHelper;
7166

7267
import com.google.gson.Gson;
7368
import io.vertx.core.MultiMap;
@@ -76,45 +71,31 @@
7671
import io.vertx.ext.web.RoutingContext;
7772
import io.vertx.ext.web.handler.impl.HttpStatusException;
7873
import org.apache.commons.lang3.StringUtils;
74+
import org.apache.commons.lang3.tuple.Pair;
7975

8076
public class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHandler {
8177

82-
// TODO extract this somewhere more appropriate
83-
public static final Template ALL_EVENTS_TEMPLATE =
84-
new Template(
85-
"ALL",
86-
"Enable all available events in the target JVM, with default option values. This will be very expensive and is intended primarily for testing Cryostat's own capabilities.",
87-
"Cryostat",
88-
TemplateType.TARGET);
89-
90-
private static final Pattern TEMPLATE_PATTERN =
91-
Pattern.compile("^template=([\\w]+)(?:,type=([\\w]+))?$");
92-
9378
static final String PATH = "targets/:targetId/recordings";
9479
private final TargetConnectionManager targetConnectionManager;
80+
private final RecordingCreationHelper recordingCreationHelper;
9581
private final RecordingOptionsBuilderFactory recordingOptionsBuilderFactory;
96-
private final EventOptionsBuilder.Factory eventOptionsBuilderFactory;
9782
private final Provider<WebServer> webServerProvider;
9883
private final Gson gson;
99-
private final NotificationFactory notificationFactory;
100-
private static final String NOTIFICATION_CATEGORY = "RecordingCreated";
10184

10285
@Inject
10386
TargetRecordingsPostHandler(
10487
AuthManager auth,
10588
TargetConnectionManager targetConnectionManager,
89+
RecordingCreationHelper recordingCreationHelper,
10690
RecordingOptionsBuilderFactory recordingOptionsBuilderFactory,
107-
EventOptionsBuilder.Factory eventOptionsBuilderFactory,
10891
Provider<WebServer> webServerProvider,
109-
Gson gson,
110-
NotificationFactory notificationFactory) {
92+
Gson gson) {
11193
super(auth);
11294
this.targetConnectionManager = targetConnectionManager;
95+
this.recordingCreationHelper = recordingCreationHelper;
11396
this.recordingOptionsBuilderFactory = recordingOptionsBuilderFactory;
114-
this.eventOptionsBuilderFactory = eventOptionsBuilderFactory;
11597
this.webServerProvider = webServerProvider;
11698
this.gson = gson;
117-
this.notificationFactory = notificationFactory;
11899
}
119100

120101
@Override
@@ -150,18 +131,11 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception {
150131
}
151132

152133
try {
153-
Optional<HyperlinkedSerializableRecordingDescriptor> descriptor =
134+
ConnectionDescriptor connectionDescriptor = getConnectionDescriptorFromContext(ctx);
135+
HyperlinkedSerializableRecordingDescriptor linkedDescriptor =
154136
targetConnectionManager.executeConnectedTask(
155-
getConnectionDescriptorFromContext(ctx),
137+
connectionDescriptor,
156138
connection -> {
157-
if (getDescriptorByName(connection, recordingName).isPresent()) {
158-
throw new HttpStatusException(
159-
400,
160-
String.format(
161-
"Recording with name \"%s\" already exists",
162-
recordingName));
163-
}
164-
165139
RecordingOptionsBuilder builder =
166140
recordingOptionsBuilderFactory
167141
.create(connection.getService())
@@ -185,57 +159,34 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception {
185159
if (attrs.contains("maxSize")) {
186160
builder = builder.maxSize(Long.parseLong(attrs.get("maxSize")));
187161
}
188-
IConstrainedMap<String> recordingOptions = builder.build();
189-
connection
190-
.getService()
191-
.start(
192-
recordingOptions,
193-
enableEvents(connection, eventSpecifier));
194-
notificationFactory
195-
.createBuilder()
196-
.metaCategory(NOTIFICATION_CATEGORY)
197-
.metaType(HttpMimeType.JSON)
198-
.message(
199-
Map.of(
200-
"recording",
201-
recordingName,
202-
"target",
203-
getConnectionDescriptorFromContext(ctx)
204-
.getTargetId()))
205-
.build()
206-
.send();
207-
return getDescriptorByName(connection, recordingName)
208-
.map(
209-
d -> {
210-
try {
211-
WebServer webServer =
212-
webServerProvider.get();
213-
return new HyperlinkedSerializableRecordingDescriptor(
214-
d,
215-
webServer.getDownloadURL(
216-
connection, d.getName()),
217-
webServer.getReportURL(
218-
connection, d.getName()));
219-
} catch (QuantityConversionException
220-
| URISyntaxException
221-
| IOException e) {
222-
throw new HttpStatusException(500, e);
223-
}
224-
});
162+
Pair<String, TemplateType> template =
163+
RecordingCreationHelper.parseEventSpecifierToTemplate(
164+
eventSpecifier);
165+
IRecordingDescriptor descriptor =
166+
recordingCreationHelper.startRecording(
167+
connectionDescriptor,
168+
builder.build(),
169+
template.getLeft(),
170+
template.getRight());
171+
try {
172+
WebServer webServer = webServerProvider.get();
173+
return new HyperlinkedSerializableRecordingDescriptor(
174+
descriptor,
175+
webServer.getDownloadURL(
176+
connection, descriptor.getName()),
177+
webServer.getReportURL(
178+
connection, descriptor.getName()));
179+
} catch (QuantityConversionException
180+
| URISyntaxException
181+
| IOException e) {
182+
throw new HttpStatusException(500, e);
183+
}
225184
});
226185

227-
descriptor.ifPresentOrElse(
228-
linkedDescriptor -> {
229-
ctx.response().setStatusCode(201);
230-
ctx.response().putHeader(HttpHeaders.LOCATION, "/" + recordingName);
231-
ctx.response()
232-
.putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.JSON.mime());
233-
ctx.response().end(gson.toJson(linkedDescriptor));
234-
},
235-
() -> {
236-
throw new HttpStatusException(
237-
500, "Unexpected failure to create recording");
238-
});
186+
ctx.response().setStatusCode(201);
187+
ctx.response().putHeader(HttpHeaders.LOCATION, "/" + recordingName);
188+
ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.JSON.mime());
189+
ctx.response().end(gson.toJson(linkedDescriptor));
239190
} catch (NumberFormatException nfe) {
240191
throw new HttpStatusException(
241192
400, String.format("Invalid argument: %s", nfe.getMessage()), nfe);
@@ -250,59 +201,4 @@ protected Optional<IRecordingDescriptor> getDescriptorByName(
250201
.filter(recording -> recording.getName().equals(recordingName))
251202
.findFirst();
252203
}
253-
254-
protected IConstrainedMap<EventOptionID> enableEvents(JFRConnection connection, String events)
255-
throws Exception {
256-
Matcher m = TEMPLATE_PATTERN.matcher(events);
257-
m.find();
258-
String templateName = m.group(1);
259-
String typeName = m.group(2);
260-
if (ALL_EVENTS_TEMPLATE.getName().equals(templateName)) {
261-
return enableAllEvents(connection);
262-
}
263-
if (typeName != null) {
264-
return connection
265-
.getTemplateService()
266-
.getEvents(templateName, TemplateType.valueOf(typeName))
267-
.orElseThrow(
268-
() ->
269-
new IllegalArgumentException(
270-
String.format(
271-
"No template \"%s\" found with type %s",
272-
templateName, typeName)));
273-
}
274-
// if template type not specified, try to find a Custom template by that name. If none,
275-
// fall back on finding a Target built-in template by the name. If not, throw an
276-
// exception and bail out.
277-
return connection
278-
.getTemplateService()
279-
.getEvents(templateName, TemplateType.CUSTOM)
280-
.or(
281-
() -> {
282-
try {
283-
return connection
284-
.getTemplateService()
285-
.getEvents(templateName, TemplateType.TARGET);
286-
} catch (Exception e) {
287-
return Optional.empty();
288-
}
289-
})
290-
.orElseThrow(
291-
() ->
292-
new IllegalArgumentException(
293-
String.format(
294-
"Invalid/unknown event template %s",
295-
templateName)));
296-
}
297-
298-
protected IConstrainedMap<EventOptionID> enableAllEvents(JFRConnection connection)
299-
throws Exception {
300-
EventOptionsBuilder builder = eventOptionsBuilderFactory.create(connection);
301-
302-
for (IEventTypeInfo eventTypeInfo : connection.getService().getAvailableEventTypes()) {
303-
builder.addEvent(eventTypeInfo.getEventTypeID().getFullKey(), "enabled", "true");
304-
}
305-
306-
return builder.build();
307-
}
308204
}

0 commit comments

Comments
 (0)