diff --git a/.github/workflows/pr-ci.yaml b/.github/workflows/pr-ci.yaml
index af5da51f0..a95e1b83b 100644
--- a/.github/workflows/pr-ci.yaml
+++ b/.github/workflows/pr-ci.yaml
@@ -270,28 +270,28 @@ jobs:
body: commentBody
});
- # compare-graphql-schema:
- # needs: [update-schemas]
- # runs-on: ubuntu-latest
- # permissions:
- # pull-requests: write
- # steps:
- # - uses: actions/download-artifact@v3
- # with:
- # name: graphql-diff
- # - name: Comment schema check result
- # uses: actions/github-script@v6
- # with:
- # script: |
- # const diffFmt = s => {
- # return "```diff\n" + s + "\n```";
- # };
- # const commentBody = ${{ needs.update-schemas.outputs.GRAPHQL_STATUS }} == '0'
- # ? `No GraphQL schema changes detected.`
- # : `GraphQL schema change detected:\n\n${diffFmt(require('fs').readFileSync('${{ needs.update-schemas.outputs.GRAPHQL_DIFF_FILE }}'))}`;
- # github.rest.issues.createComment({
- # issue_number: context.issue.number,
- # owner: context.repo.owner,
- # repo: context.repo.repo,
- # body: commentBody
- # });
+ compare-graphql-schema:
+ needs: [update-schemas]
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/download-artifact@v3
+ with:
+ name: graphql-diff
+ - name: Comment schema check result
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const diffFmt = s => {
+ return "```diff\n" + s + "\n```";
+ };
+ const commentBody = ${{ needs.update-schemas.outputs.GRAPHQL_STATUS }} == '0'
+ ? `No GraphQL schema changes detected.`
+ : `GraphQL schema change detected:\n\n${diffFmt(require('fs').readFileSync('${{ needs.update-schemas.outputs.GRAPHQL_DIFF_FILE }}'))}`;
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: commentBody
+ });
diff --git a/compose/cryostat.yml b/compose/cryostat.yml
index f77c3167a..a1f4145e6 100644
--- a/compose/cryostat.yml
+++ b/compose/cryostat.yml
@@ -29,9 +29,9 @@ services:
QUARKUS_HTTP_HOST: "cryostat"
QUARKUS_HTTP_PORT: ${CRYOSTAT_HTTP_PORT}
QUARKUS_HIBERNATE_ORM_LOG_SQL: "true"
- CRYOSTAT_DISCOVERY_JDP_ENABLED: "true"
- CRYOSTAT_DISCOVERY_PODMAN_ENABLED: "true"
- CRYOSTAT_DISCOVERY_DOCKER_ENABLED: "true"
+ CRYOSTAT_DISCOVERY_JDP_ENABLED: ${CRYOSTAT_DISCOVERY_JDP_ENABLED:-true}
+ CRYOSTAT_DISCOVERY_PODMAN_ENABLED: ${CRYOSTAT_DISCOVERY_PODMAN_ENABLED:-true}
+ CRYOSTAT_DISCOVERY_DOCKER_ENABLED: ${CRYOSTAT_DISCOVERY_DOCKER_ENABLED:-true}
JAVA_OPTS_APPEND: "-XX:+FlightRecorder -XX:StartFlightRecording=name=onstart,settings=default,disk=true,maxage=5m -XX:StartFlightRecording=name=startup,settings=profile,disk=true,duration=30s -Dcom.sun.management.jmxremote.autodiscovery=true -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false"
restart: unless-stopped
healthcheck:
diff --git a/pom.xml b/pom.xml
index 42e6e8d9a..9c38d68ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,6 +108,10 @@
io.quarkus
quarkus-smallrye-openapi
+
+ io.quarkus
+ quarkus-smallrye-graphql
+
io.smallrye.reactive
smallrye-mutiny-vertx-web-client
diff --git a/schema/openapi.yaml b/schema/openapi.yaml
index fcda791f2..4ce4608a6 100644
--- a/schema/openapi.yaml
+++ b/schema/openapi.yaml
@@ -31,6 +31,8 @@ components:
type: integer
downloadUrl:
type: string
+ jvmId:
+ type: string
metadata:
$ref: '#/components/schemas/Metadata'
name:
@@ -382,6 +384,31 @@ paths:
- SecurityScheme: []
tags:
- Recordings
+ /api/beta/fs/recordings/{jvmId}:
+ get:
+ parameters:
+ - in: path
+ name: jvmId
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ items:
+ $ref: '#/components/schemas/ArchivedRecordingDirectory'
+ type: array
+ description: OK
+ "401":
+ description: Not Authorized
+ "403":
+ description: Not Allowed
+ security:
+ - SecurityScheme: []
+ tags:
+ - Recordings
/api/beta/fs/recordings/{jvmId}/{filename}:
delete:
parameters:
@@ -519,6 +546,30 @@ paths:
- SecurityScheme: []
tags:
- Recordings
+ /api/beta/recordings/{connectUrl}/{filename}/upload:
+ post:
+ parameters:
+ - in: path
+ name: connectUrl
+ required: true
+ schema:
+ type: string
+ - in: path
+ name: filename
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ "401":
+ description: Not Authorized
+ "403":
+ description: Not Allowed
+ security:
+ - SecurityScheme: []
+ tags:
+ - Recordings
/api/beta/recordings/{jvmId}:
get:
parameters:
@@ -1242,6 +1293,31 @@ paths:
description: OK
tags:
- Discovery
+ /api/v2.2/graphql:
+ get:
+ responses:
+ "200":
+ description: OK
+ "401":
+ description: Not Authorized
+ "403":
+ description: Not Allowed
+ security:
+ - SecurityScheme: []
+ tags:
+ - Graph QL
+ post:
+ responses:
+ "200":
+ description: OK
+ "401":
+ description: Not Authorized
+ "403":
+ description: Not Allowed
+ security:
+ - SecurityScheme: []
+ tags:
+ - Graph QL
/api/v2/rules:
get:
responses:
diff --git a/schema/schema.graphql b/schema/schema.graphql
new file mode 100644
index 000000000..69284ffef
--- /dev/null
+++ b/schema/schema.graphql
@@ -0,0 +1,304 @@
+type ActiveRecording {
+ continuous: Boolean!
+ "Archive the specified Flight Recording"
+ doArchive: ArchivedRecording
+ "Delete the specified Flight Recording"
+ doDelete: ActiveRecording
+ "Updates the metadata labels for an existing Flight Recording."
+ doPutMetadata(metadataInput: MetadataLabelsInput): ActiveRecording
+ "Stop the specified Flight Recording"
+ doStop: ActiveRecording
+ "URL for GET request to retrieve the JFR binary file content of this recording"
+ downloadUrl: String
+ duration: BigInteger!
+ id: BigInteger
+ maxAge: BigInteger!
+ maxSize: BigInteger!
+ metadata: Metadata!
+ name: String!
+ remoteId: BigInteger!
+ "URL for GET request to retrieve a JSON formatted Automated Analysis Report of this recording"
+ reportUrl: String
+ startTime: BigInteger!
+ state: RecordingState!
+ target: Target!
+ toDisk: Boolean!
+}
+
+type ActiveRecordings {
+ aggregate: AggregateInfo!
+ data: [ActiveRecording]!
+}
+
+type AggregateInfo {
+ "The number of elements in this collection"
+ count: BigInteger!
+ "The sum of sizes of elements in this collection, or 0 if not applicable"
+ size: BigInteger!
+}
+
+type Annotations {
+ cryostat(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]
+ platform(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]
+}
+
+type ArchivedRecording {
+ archivedTime: BigInteger!
+ doDelete: ArchivedRecording!
+ doPutMetadata(metadataInput: MetadataLabelsInput): ArchivedRecording!
+ "URL for GET request to retrieve the JFR binary file content of this recording"
+ downloadUrl: String
+ jvmId: String
+ metadata: Metadata
+ name: String
+ "URL for GET request to retrieve a JSON formatted Automated Analysis Report of this recording"
+ reportUrl: String
+ size: BigInteger!
+}
+
+type ArchivedRecordings {
+ aggregate: AggregateInfo!
+ data: [ArchivedRecording]!
+}
+
+type DiscoveryNode {
+ children: [DiscoveryNode]
+ "Get target nodes that are descendants of this node. That is, get the set of leaf nodes from anywhere below this node's subtree."
+ descendantTargets(filter: DiscoveryNodeFilterInput): [DiscoveryNode]
+ id: BigInteger
+ labels(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]!
+ name: String!
+ nodeType: String!
+ target: Target
+}
+
+type Entry_String_String {
+ key: String
+ value: String
+}
+
+type MBeanMetrics {
+ jvmId: String
+ memory: MemoryMetrics
+ os: OperatingSystemMetrics
+ runtime: RuntimeMetrics
+ thread: ThreadMetrics
+}
+
+type MemoryMetrics {
+ freeHeapMemory: BigInteger!
+ freeNonHeapMemory: BigInteger!
+ heapMemoryUsage: MemoryUtilization
+ heapMemoryUsagePercent: Float!
+ nonHeapMemoryUsage: MemoryUtilization
+ objectPendingFinalizationCount: BigInteger!
+ verbose: Boolean!
+}
+
+type MemoryUtilization {
+ committed: BigInteger!
+ init: BigInteger!
+ max: BigInteger!
+ used: BigInteger!
+}
+
+type Metadata {
+ "ISO-8601"
+ expiry: DateTime
+ labels(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]
+}
+
+"Mutation root"
+type Mutation {
+ "Archive an existing Flight Recording matching the given filter, on all Targets under the subtrees of the discovery nodes matching the given filter"
+ archiveRecording(nodes: DiscoveryNodeFilterInput!, recordings: ActiveRecordingsFilterInput): [ArchivedRecording]
+ "Start a new Flight Recording on all Targets under the subtrees of the discovery nodes matching the given filter"
+ createRecording(nodes: DiscoveryNodeFilterInput!, recording: RecordingSettingsInput!): [ActiveRecording]
+ "Create a Flight Recorder Snapshot on all Targets under the subtrees of the discovery nodes matching the given filter"
+ createSnapshot(nodes: DiscoveryNodeFilterInput!): [ActiveRecording]
+ "Delete an existing Flight Recording matching the given filter, on all Targets under the subtrees of the discovery nodes matching the given filter"
+ deleteRecording(nodes: DiscoveryNodeFilterInput!, recordings: ActiveRecordingsFilterInput): [ActiveRecording]
+ "Stop an existing Flight Recording matching the given filter, on all Targets under the subtrees of the discovery nodes matching the given filter"
+ stopRecording(nodes: DiscoveryNodeFilterInput!, recordings: ActiveRecordingsFilterInput): [ActiveRecording]
+}
+
+type OperatingSystemMetrics {
+ arch: String
+ availableProcessors: Int!
+ committedVirtualMemorySize: BigInteger!
+ freePhysicalMemorySize: BigInteger!
+ freeSwapSpaceSize: BigInteger!
+ name: String
+ processCpuLoad: Float!
+ processCpuTime: BigInteger!
+ systemCpuLoad: Float!
+ systemLoadAverage: Float!
+ totalPhysicalMemorySize: BigInteger!
+ totalSwapSpaceSize: BigInteger!
+ version: String
+}
+
+"Query root"
+type Query {
+ archivedRecordings(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
+ "Get all environment nodes in the discovery tree with optional filtering"
+ environmentNodes(filter: DiscoveryNodeFilterInput): [DiscoveryNode]
+ "Get the root target discovery node"
+ rootNode: DiscoveryNode
+ "Get the Target discovery nodes, i.e. the leaf nodes of the discovery tree"
+ targetNodes(filter: DiscoveryNodeFilterInput): [DiscoveryNode]
+}
+
+type Recordings {
+ active(filter: ActiveRecordingsFilterInput): ActiveRecordings
+ archived(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
+}
+
+type RuntimeMetrics {
+ bootClassPath: String
+ bootClassPathSupported: Boolean!
+ classPath: String
+ inputArguments: [String]
+ libraryPath: String
+ managementSpecVersion: String
+ name: String
+ specName: String
+ specVendor: String
+ specVersion: String
+ startTime: BigInteger!
+ systemProperties(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]
+ uptime: BigInteger!
+ vmName: String
+ vmVendor: String
+ vmVersion: String
+}
+
+type Target {
+ activeRecordings(filter: ActiveRecordingsFilterInput): ActiveRecordings
+ agent: Boolean!
+ alias: String!
+ annotations: Annotations!
+ archivedRecordings(filter: ArchivedRecordingsFilterInput): ArchivedRecordings
+ connectUrl: String!
+ "Create a new Flight Recorder Snapshot on the specified Target"
+ doSnapshot: ActiveRecording
+ "Start a new Flight Recording on the specified Target"
+ doStartRecording(recording: RecordingSettingsInput!): ActiveRecording
+ id: BigInteger
+ jvmId: String
+ labels(
+ "Get entry/entries for a certain key/s"
+ key: [String]
+ ): [Entry_String_String]!
+ "Get live MBean metrics snapshot from the specified Target"
+ mbeanMetrics: MBeanMetrics
+ "Get the active and archived recordings belonging to this target"
+ recordings: Recordings
+}
+
+type ThreadMetrics {
+ allThreadIds: [BigInteger]
+ currentThreadCpuTime: BigInteger!
+ currentThreadCpuTimeSupported: Boolean!
+ currentThreadUserTime: BigInteger!
+ daemonThreadCount: Int!
+ objectMonitorUsageSupported: Boolean!
+ peakThreadCount: Int!
+ synchronizerUsageSupported: Boolean!
+ threadContentionMonitoringEnabled: Boolean!
+ threadContentionMonitoringSupported: Boolean!
+ threadCount: Int!
+ threadCpuTimeEnabled: Boolean!
+ threadCpuTimeSupported: Boolean!
+ totalStartedThreadCount: BigInteger!
+}
+
+"Running state of an active Flight Recording"
+enum RecordingState {
+ "CLOSED"
+ CLOSED
+ "DELAYED"
+ DELAYED
+ "NEW"
+ NEW
+ "RUNNING"
+ RUNNING
+ "STOPPED"
+ STOPPED
+}
+
+input ActiveRecordingsFilterInput {
+ continuous: Boolean
+ durationMsGreaterThanEqual: BigInteger
+ durationMsLessThanEqual: BigInteger
+ labels: [String]
+ name: String
+ names: [String]
+ startTimeMsAfterEqual: BigInteger
+ startTimeMsBeforeEqual: BigInteger
+ state: RecordingState
+ toDisk: Boolean
+}
+
+input ArchivedRecordingsFilterInput {
+ archivedTimeAfterEqual: BigInteger
+ archivedTimeBeforeEqual: BigInteger
+ labels: [String]
+ name: String
+ names: [String]
+ sizeBytesGreaterThanEqual: BigInteger
+ sizeBytesLessThanEqual: BigInteger
+ sourceTarget: String
+}
+
+input DiscoveryNodeFilterInput {
+ annotations: [String]
+ id: BigInteger
+ ids: [BigInteger]
+ labels: [String]
+ name: String
+ names: [String]
+ nodeTypes: [String]
+}
+
+input Entry_String_StringInput {
+ key: String
+ value: String
+}
+
+input MetadataLabelsInput {
+ labels: [Entry_String_StringInput]
+}
+
+input RecordingMetadataInput {
+ labels: [Entry_String_StringInput]
+}
+
+input RecordingSettingsInput {
+ archiveOnStop: Boolean
+ continuous: Boolean
+ duration: BigInteger
+ maxAge: BigInteger
+ maxSize: BigInteger
+ metadata: RecordingMetadataInput
+ name: String!
+ replace: String
+ template: String!
+ templateType: String!
+ toDisk: Boolean
+}
diff --git a/schema/update.bash b/schema/update.bash
index 07d109017..6ace30cca 100755
--- a/schema/update.bash
+++ b/schema/update.bash
@@ -24,4 +24,4 @@ while true; do
fi
done
wget http://localhost:8181/api -O - | yq -P 'sort_keys(..)' > "${DIR}/openapi.yaml"
-# wget http://localhost:8181/api/v3/graphql/schema.graphql -O "${DIR}/schema.graphql"
+wget http://localhost:8181/api/v3/graphql/schema.graphql -O "${DIR}/schema.graphql"
diff --git a/src/main/java/io/cryostat/JsonRequestFilter.java b/src/main/java/io/cryostat/JsonRequestFilter.java
index 36dec2f84..5c9109554 100644
--- a/src/main/java/io/cryostat/JsonRequestFilter.java
+++ b/src/main/java/io/cryostat/JsonRequestFilter.java
@@ -37,7 +37,12 @@ public class JsonRequestFilter implements ContainerRequestFilter {
static final Set disallowedFields = Set.of("id");
static final Set allowedPathPatterns =
- Set.of("/api/v2.2/discovery", "/api/v2/rules/[\\w]+", "/api/beta/matchExpressions");
+ Set.of(
+ "/api/v2.2/discovery",
+ "/api/v2/rules/[\\w]+",
+ "/api/beta/matchExpressions",
+ "/api/v2.2/graphql",
+ "/api/v3/graphql");
private final Map compiledPatterns = new HashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
diff --git a/src/main/java/io/cryostat/ObjectMapperCustomization.java b/src/main/java/io/cryostat/ObjectMapperCustomization.java
new file mode 100644
index 000000000..7284b0282
--- /dev/null
+++ b/src/main/java/io/cryostat/ObjectMapperCustomization.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
+import com.fasterxml.jackson.databind.type.MapType;
+import io.quarkus.jackson.ObjectMapperCustomizer;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ObjectMapperCustomization implements ObjectMapperCustomizer {
+
+ @Override
+ public void customize(ObjectMapper objectMapper) {
+ // FIXME get this version information from the maven build somehow
+ SimpleModule mapModule =
+ new SimpleModule(
+ "MapSerialization", new Version(3, 0, 0, null, "io.cryostat", "cryostat"));
+
+ mapModule.setSerializerModifier(new MapSerializerModifier());
+
+ objectMapper.registerModule(mapModule);
+ }
+
+ static class MapSerializerModifier extends BeanSerializerModifier {
+ @Override
+ public JsonSerializer> modifyMapSerializer(
+ SerializationConfig config,
+ MapType valueType,
+ BeanDescription beanDesc,
+ JsonSerializer serializer) {
+ if (valueType.getKeyType().getRawClass().equals(String.class)
+ && valueType.getContentType().getRawClass().equals(String.class)) {
+ return new MapSerializer();
+ }
+ return serializer;
+ }
+ }
+
+ static class MapSerializer extends JsonSerializer