Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphql): prototyping #783

Merged
merged 51 commits into from
Feb 11, 2022
Merged

Conversation

andrewazores
Copy link
Member

Related to #495

@andrewazores andrewazores added the feat New feature or request label Dec 10, 2021
@andrewazores andrewazores force-pushed the graphql branch 3 times, most recently from 965a192 to 49fcb30 Compare December 15, 2021 19:07
@andrewazores andrewazores force-pushed the graphql branch 2 times, most recently from 7e008e6 to d4087a1 Compare January 20, 2022 18:32
@andrewazores
Copy link
Member Author

@hareetd @jan-law @ebaron any questions/thoughts/concerns about GraphQL and the capabilities we have with it so far in this draft?

(The code is a mess - it definitely should not just all be stuffed into a GraphModule, so before merging any of this I would refactor it out and split it up in some way generally similar to how the HttpModule and RequestHandlers are.)

@andrewazores
Copy link
Member Author

Also, this looks like a reasonable approach to the TODO I have left in queries.graphqls:

https://www.yld.io/blog/query-by-2-or-more-fields-on-graphql/

I'm still not sure if this should be an AND or OR or if we want to make that a selectable option. GraphQL doesn't like nested structures, at least not ones to arbitrary depth, so we can't really do something like building an expression out of the label/annotation selector (key/value match pair) with that. More feasibly we could have the query support a list of operands and allow the user to select whether they are ANDed or ORed - ie any of the conditions matches or all must match for a target to be included in the result set.

@andrewazores
Copy link
Member Author

Just had a neat idea to really use the power of the GraphQL engine to our advantage here, after I've gone through and done some refactoring and cleanup to make this more presentable. Check it out:

$ https -v :8181/api/beta/graphql query=@graphql/start-recording-by-target.graphql
query {
    targetsDescendedFrom(nodes: [{ name: "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi", nodeType: "JVM" }]) {
        name
        nodeType
        target {
            alias
            serviceUri
            labels
            annotations {
                platform
                cryostat
            }
        }
        recordings {
            active {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
            }
            archived {
                name
                downloadUrl
                reportUrl
            }
        }
        startRecording(recording: {
            name: "foo",
            template: "Profiling",
            templateType: "TARGET",
            duration: 30
            }) {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
        }
    }
}

This happens to be specifying a specific target node (the 9093 vertx-fib-demo) as the node whose descendants should be queried, so in this case the list of target descendants is trivially just the singleton list of the specified node itself. The query will be carried out against all of the nodes in that list and it will return a whack of information about each of those nodes, like its name and type and labels, etc. The GraphQL engine will also call another data fetcher to resolve the recordings field of the query, which causes Cryostat to connect to the target and check what its active recordings are, so the query includes information about those recordings' name, downloadUrl, reportUrl, etc., and it will also include the equivalent information for archived recordings on disk belonging to each target.

Most notably and new in the latest commit is the startRecording "field" of the query, which is obviously actually a mutating function call with each target node in the descendants list as the source object. This means that a client can make a read-only query or a mutating query using the same "shape" of query at the top level, starting with the targetsDescendedFrom function, but by adding the startRecording field it can become a mutating query that GraphQL will actually call out to another data fetcher, causing Cryostat to connect again to each of the target nodes and start a recording on them with the given settings. The result of this mutation is a HyperlinkedSerializableRecordingDescriptor, so the client can ask again for whichever fields of that they would like in the response.

Speaking of response, here's what the response to the big query above looks like:

{
    "data": {
        "targetsDescendedFrom": [
            {
                "name": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi",
                "nodeType": "JVM",
                "recordings": {
                    "active": [],
                    "archived": []
                },
                "startRecording": {
                    "continuous": false,
                    "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/foo",
                    "duration": 30000,
                    "maxAge": 0,
                    "maxSize": 0,
                    "name": "foo",
                    "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/foo",
                    "startTime": 1643340857661,
                    "state": "RUNNING",
                    "toDisk": true
                },
                "target": {
                    "alias": "es.andrewazor.demo.Main",
                    "annotations": {
                        "cryostat": {
                            "HOST": "cryostat",
                            "JAVA_MAIN": "es.andrewazor.demo.Main",
                            "PORT": "9093"
                        },
                        "platform": {}
                    },
                    "labels": {},
                    "serviceUri": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi"
                }
            }
        ]
    }
}

We can see that the active recordings list is empty, so this means (and this is also supported by the logs) that the read part of the query was executed before the mutation part. But if we simply reorder the components of the query then we can cause the mutation to happen first so that the query includes that result as well.

query {
    targetsDescendedFrom(nodes: [{ name: "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi", nodeType: "JVM" }]) {
        startRecording(recording: {
            name: "bar",
            template: "Profiling",
            templateType: "TARGET",
            duration: 30
            }) {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
        }
        name
        nodeType
        target {
            alias
            serviceUri
            labels
            annotations {
                platform
                cryostat
            }
        }
        recordings {
            active {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
            }
            archived {
                name
                downloadUrl
                reportUrl
            }
        }
    }
}
{
    "data": {
        "targetsDescendedFrom": [
            {
                "name": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi",
                "nodeType": "JVM",
                "recordings": {
                    "active": [
                        {
                            "continuous": false,
                            "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/foo",
                            "duration": 30000,
                            "maxAge": 0,
                            "maxSize": 0,
                            "name": "foo",
                            "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/foo",
                            "startTime": 1643340857661,
                            "state": "STOPPED",
                            "toDisk": true
                        },
                        {
                            "continuous": false,
                            "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/bar",
                            "duration": 30000,
                            "maxAge": 0,
                            "maxSize": 0,
                            "name": "bar",
                            "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/bar",
                            "startTime": 1643341706359,
                            "state": "RUNNING",
                            "toDisk": true
                        }
                    ],
                    "archived": []
                },
                "startRecording": {
                    "continuous": false,
                    "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/bar",
                    "duration": 30000,
                    "maxAge": 0,
                    "maxSize": 0,
                    "name": "bar",
                    "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/bar",
                    "startTime": 1643341706359,
                    "state": "RUNNING",
                    "toDisk": true
                },
                "target": {
                    "alias": "es.andrewazor.demo.Main",
                    "annotations": {
                        "cryostat": {
                            "HOST": "cryostat",
                            "JAVA_MAIN": "es.andrewazor.demo.Main",
                            "PORT": "9093"
                        },
                        "platform": {}
                    },
                    "labels": {},
                    "serviceUri": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi"
                }
            }
        ]
    }
}

@andrewazores andrewazores force-pushed the graphql branch 5 times, most recently from a49a7e9 to a23c33e Compare February 3, 2022 17:19
@andrewazores andrewazores marked this pull request as ready for review February 3, 2022 19:32
@andrewazores andrewazores force-pushed the graphql branch 3 times, most recently from c1dcf14 to 59b5126 Compare February 8, 2022 20:21
Copy link
Member

@ebaron ebaron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes seem good to me. Lots of FIXMEs/TODOs throughout, but this is mostly self-contained, so I'm not worried about getting this in and fixing it up later.

@hareetd
Copy link
Contributor

hareetd commented Mar 8, 2022

Just had a neat idea to really use the power of the GraphQL engine to our advantage here, after I've gone through and done some refactoring and cleanup to make this more presentable. Check it out:

$ https -v :8181/api/beta/graphql query=@graphql/start-recording-by-target.graphql
query {
    targetsDescendedFrom(nodes: [{ name: "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi", nodeType: "JVM" }]) {
        name
        nodeType
        target {
            alias
            serviceUri
            labels
            annotations {
                platform
                cryostat
            }
        }
        recordings {
            active {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
            }
            archived {
                name
                downloadUrl
                reportUrl
            }
        }
        startRecording(recording: {
            name: "foo",
            template: "Profiling",
            templateType: "TARGET",
            duration: 30
            }) {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
        }
    }
}

This happens to be specifying a specific target node (the 9093 vertx-fib-demo) as the node whose descendants should be queried, so in this case the list of target descendants is trivially just the singleton list of the specified node itself. The query will be carried out against all of the nodes in that list and it will return a whack of information about each of those nodes, like its name and type and labels, etc. The GraphQL engine will also call another data fetcher to resolve the recordings field of the query, which causes Cryostat to connect to the target and check what its active recordings are, so the query includes information about those recordings' name, downloadUrl, reportUrl, etc., and it will also include the equivalent information for archived recordings on disk belonging to each target.

Most notably and new in the latest commit is the startRecording "field" of the query, which is obviously actually a mutating function call with each target node in the descendants list as the source object. This means that a client can make a read-only query or a mutating query using the same "shape" of query at the top level, starting with the targetsDescendedFrom function, but by adding the startRecording field it can become a mutating query that GraphQL will actually call out to another data fetcher, causing Cryostat to connect again to each of the target nodes and start a recording on them with the given settings. The result of this mutation is a HyperlinkedSerializableRecordingDescriptor, so the client can ask again for whichever fields of that they would like in the response.

Speaking of response, here's what the response to the big query above looks like:

{
    "data": {
        "targetsDescendedFrom": [
            {
                "name": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi",
                "nodeType": "JVM",
                "recordings": {
                    "active": [],
                    "archived": []
                },
                "startRecording": {
                    "continuous": false,
                    "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/foo",
                    "duration": 30000,
                    "maxAge": 0,
                    "maxSize": 0,
                    "name": "foo",
                    "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/foo",
                    "startTime": 1643340857661,
                    "state": "RUNNING",
                    "toDisk": true
                },
                "target": {
                    "alias": "es.andrewazor.demo.Main",
                    "annotations": {
                        "cryostat": {
                            "HOST": "cryostat",
                            "JAVA_MAIN": "es.andrewazor.demo.Main",
                            "PORT": "9093"
                        },
                        "platform": {}
                    },
                    "labels": {},
                    "serviceUri": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi"
                }
            }
        ]
    }
}

We can see that the active recordings list is empty, so this means (and this is also supported by the logs) that the read part of the query was executed before the mutation part. But if we simply reorder the components of the query then we can cause the mutation to happen first so that the query includes that result as well.

query {
    targetsDescendedFrom(nodes: [{ name: "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi", nodeType: "JVM" }]) {
        startRecording(recording: {
            name: "bar",
            template: "Profiling",
            templateType: "TARGET",
            duration: 30
            }) {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
        }
        name
        nodeType
        target {
            alias
            serviceUri
            labels
            annotations {
                platform
                cryostat
            }
        }
        recordings {
            active {
                name
                downloadUrl
                reportUrl
                state
                startTime
                duration
                continuous
                toDisk
                maxSize
                maxAge
            }
            archived {
                name
                downloadUrl
                reportUrl
            }
        }
    }
}
{
    "data": {
        "targetsDescendedFrom": [
            {
                "name": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi",
                "nodeType": "JVM",
                "recordings": {
                    "active": [
                        {
                            "continuous": false,
                            "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/foo",
                            "duration": 30000,
                            "maxAge": 0,
                            "maxSize": 0,
                            "name": "foo",
                            "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/foo",
                            "startTime": 1643340857661,
                            "state": "STOPPED",
                            "toDisk": true
                        },
                        {
                            "continuous": false,
                            "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/bar",
                            "duration": 30000,
                            "maxAge": 0,
                            "maxSize": 0,
                            "name": "bar",
                            "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/bar",
                            "startTime": 1643341706359,
                            "state": "RUNNING",
                            "toDisk": true
                        }
                    ],
                    "archived": []
                },
                "startRecording": {
                    "continuous": false,
                    "downloadUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/recordings/bar",
                    "duration": 30000,
                    "maxAge": 0,
                    "maxSize": 0,
                    "name": "bar",
                    "reportUrl": "https://localhost:8181/api/v1/targets/service:jmx:rmi:%2F%2F%2Fjndi%2Frmi:%2F%2Fcryostat:9093%2Fjmxrmi/reports/bar",
                    "startTime": 1643341706359,
                    "state": "RUNNING",
                    "toDisk": true
                },
                "target": {
                    "alias": "es.andrewazor.demo.Main",
                    "annotations": {
                        "cryostat": {
                            "HOST": "cryostat",
                            "JAVA_MAIN": "es.andrewazor.demo.Main",
                            "PORT": "9093"
                        },
                        "platform": {}
                    },
                    "labels": {},
                    "serviceUri": "service:jmx:rmi:///jndi/rmi://cryostat:9093/jmxrmi"
                }
            }
        ]
    }
}

I've been looking over this code and playing around with GraphQL on the main branch to refresh my memory before giving input on #825. I decided to try this query out but got an error saying Validation error of type FieldUndefined: Field 'targetsDescendedFrom' in type 'Query' is undefined @ 'targetsDescendedFrom'. I added TypeRuntimeWiring connecting the targetsDescendedFrom query with the TargetDescendentsFetcher as well as the necessary Query/Type definitions and now it's working. I might be doing something wrong when it comes to calling the query but if I'm not, would you like me to open an issue with a PR of my changes?

@andrewazores
Copy link
Member Author

andrewazores commented Mar 9, 2022

You didn't do anything wrong there really, but that old comment is out of date and doesn't reflect what got merged anymore. Generally though, that query was first written for making a top-level query that would select some EnvironmentNode in the tree and return a list of all of its TargetNode children. That got replaced by the top-level environmentNodes query that takes the whole tree, flattens it into a list, and removes the TargetNodes. This can then be combined with a nested descendantTargets query to list or perform actions on targets that descend from the EnvironmentNodes returned by the outer query. With the right query filter provided to the outer query you get a singleton list, and the combination outer/inner query is equivalent to the targetsDescendedFrom.

graphql/environment-nodes-query-1.graphql

query {
    environmentNodes(filter: { name: "JDP" }) {
        descendantTargets {
            doStartRecording(recording: {
                name: "foo",
                template: "Profiling",
                templateType: "TARGET",
                duration: 30
                }) {
                    name
                    state
            }
        }
    }
}

@hareetd
Copy link
Contributor

hareetd commented Mar 9, 2022

Ah makes sense, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants