diff --git a/.gitmodules b/.gitmodules index 49078789..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "hypertrace-core-graphql"] - path = hypertrace-core-graphql - url = https://github.com/hypertrace/hypertrace-core-graphql.git - branch = main diff --git a/hypertrace-core-graphql b/hypertrace-core-graphql deleted file mode 160000 index 15da6ece..00000000 --- a/hypertrace-core-graphql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 15da6ece8c20b10ef0f9875722cd77716b82eae7 diff --git a/hypertrace-core-graphql/.github/CODEOWNERS b/hypertrace-core-graphql/.github/CODEOWNERS new file mode 100644 index 00000000..77da5d32 --- /dev/null +++ b/hypertrace-core-graphql/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Each line is a file pattern followed by one or more owners. + +# global +* @hypertrace/graphql + +# GH action +.github/ @hypertrace/ci-owners \ No newline at end of file diff --git a/hypertrace-core-graphql/.github/PULL_REQUEST_TEMPLATE.md b/hypertrace-core-graphql/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..a8e827b1 --- /dev/null +++ b/hypertrace-core-graphql/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## Description +Please include a summary of the change, motivation and context. + + + + +### Testing +Please describe the tests that you ran to verify your changes. Please summarize what did you test and what needs to be tested e.g. deployed and tested helm chart locally. + +### Checklist: +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] Any dependent changes have been merged and published in downstream modules + +### Documentation +Make sure that you have documented corresponding changes in this repository or [hypertrace docs repo](https://github.com/hypertrace/hypertrace-docs-website) if required. + + diff --git a/hypertrace-core-graphql/.github/workflows/merge-publish.yml b/hypertrace-core-graphql/.github/workflows/merge-publish.yml new file mode 100644 index 00000000..95e242d7 --- /dev/null +++ b/hypertrace-core-graphql/.github/workflows/merge-publish.yml @@ -0,0 +1,30 @@ +name: merge-publish +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + merge-publish: + runs-on: ubuntu-22.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_READ_USER }} + password: ${{ secrets.DOCKERHUB_READ_TOKEN }} + + - name: push docker image + uses: hypertrace/github-actions/gradle@main + with: + args: dockerPushImages + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUBLISH_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUBLISH_TOKEN }} diff --git a/hypertrace-core-graphql/.github/workflows/pr-build.yml b/hypertrace-core-graphql/.github/workflows/pr-build.yml new file mode 100644 index 00000000..175f4dc7 --- /dev/null +++ b/hypertrace-core-graphql/.github/workflows/pr-build.yml @@ -0,0 +1,45 @@ +name: build and validate +on: + push: + branches: + - main + pull_request_target: + branches: + - main + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Check out code + uses: actions/checkout@v3 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_READ_USER }} + password: ${{ secrets.DOCKERHUB_READ_TOKEN }} + + - name: Build with Gradle + uses: hypertrace/github-actions/gradle@main + with: + args: assemble dockerBuildImages + + validate-helm-charts: + runs-on: ubuntu-22.04 + steps: + - name: Check out code + uses: actions/checkout@v3 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + fetch-depth: 0 + + - name: validate charts + uses: hypertrace/github-actions/validate-charts@main + diff --git a/hypertrace-core-graphql/.github/workflows/pr-test.yml b/hypertrace-core-graphql/.github/workflows/pr-test.yml new file mode 100644 index 00000000..d01a3efc --- /dev/null +++ b/hypertrace-core-graphql/.github/workflows/pr-test.yml @@ -0,0 +1,39 @@ +name: test +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-22.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Unit test and other verification + uses: hypertrace/github-actions/gradle@main + with: + args: check jacocoTestReport + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + name: unit test reports + flags: unit + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + files: ./**/build/test-results/**/*.xml + dependency-check: + runs-on: ubuntu-22.04 + steps: + - name: Dependency Check + uses: hypertrace/github-actions/dependency-check@main \ No newline at end of file diff --git a/hypertrace-core-graphql/.github/workflows/publish.yml b/hypertrace-core-graphql/.github/workflows/publish.yml new file mode 100644 index 00000000..d5525ffd --- /dev/null +++ b/hypertrace-core-graphql/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: Publish artifacts +on: +# Will only run when release is published. + release: + types: + - created + workflow_dispatch: + +jobs: + publish-artifacts: + runs-on: ubuntu-22.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_READ_USER }} + password: ${{ secrets.DOCKERHUB_READ_TOKEN }} + + - name: publish docker image + uses: hypertrace/github-actions/gradle@main + with: + args: dockerPushImages + env: + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_PUBLISH_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PUBLISH_TOKEN }} + + publish-helm-charts: + needs: publish-artifacts + runs-on: ubuntu-22.04 + steps: + # Set fetch-depth: 0 to fetch commit history and tags for use in version calculation + - name: Checkout Repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: package and release charts + uses: hypertrace/github-actions/helm-gcs-publish@main + with: + helm-gcs-credentials: ${{ secrets.HELM_GCS_CREDENTIALS }} + helm-gcs-repository: ${{ secrets.HELM_GCS_REPOSITORY }} + diff --git a/hypertrace-core-graphql/.github/workflows/update-locks.yml b/hypertrace-core-graphql/.github/workflows/update-locks.yml new file mode 100644 index 00000000..6fefa95b --- /dev/null +++ b/hypertrace-core-graphql/.github/workflows/update-locks.yml @@ -0,0 +1,30 @@ +name: Update Locks +on: + workflow_dispatch: + schedule: + - cron: '12 12 * * 5' +jobs: + update-versions: + runs-on: ubuntu-22.04 + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Calculate simple repository name + id: repo-basename + shell: bash + run: | + echo "value=`basename ${{ github.repository }}`" >> $GITHUB_OUTPUT + - name: Get Token from Github App + uses: tibdex/github-app-token@v2 + id: generate-token + with: + app_id: ${{ secrets.GH_CI_APP_ID }} + private_key: ${{ secrets.GH_CI_APP_PRIVATE_KEY }} + repositories: >- + [${{ toJson(steps.repo-basename.outputs.value) }}] + - name: Update locks if needed + uses: hypertrace/github-actions/raise-lock-pr@main + with: + token: ${{ steps.generate-token.outputs.token }} \ No newline at end of file diff --git a/hypertrace-core-graphql/.gitignore b/hypertrace-core-graphql/.gitignore new file mode 100644 index 00000000..1f93dfcf --- /dev/null +++ b/hypertrace-core-graphql/.gitignore @@ -0,0 +1,7 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea diff --git a/hypertrace-core-graphql/LICENSE.txt b/hypertrace-core-graphql/LICENSE.txt new file mode 100644 index 00000000..3af859d2 --- /dev/null +++ b/hypertrace-core-graphql/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Traceable Inc + + 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. diff --git a/hypertrace-core-graphql/README.md b/hypertrace-core-graphql/README.md new file mode 100644 index 00000000..3b5a5576 --- /dev/null +++ b/hypertrace-core-graphql/README.md @@ -0,0 +1,15 @@ +# Hypertrace Core GraphQL + + +## Testing + +`./gradlew test` + +## Running + +`./gradlew run` + +## Ports + +GraphQL Service runs on port 23431 at `/graphql` by default + diff --git a/hypertrace-core-graphql/build.gradle.kts b/hypertrace-core-graphql/build.gradle.kts new file mode 100644 index 00000000..e44512e4 --- /dev/null +++ b/hypertrace-core-graphql/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(commonLibs.plugins.hypertrace.ciutils) + alias(commonLibs.plugins.hypertrace.codestyle) apply false + alias(commonLibs.plugins.owasp.dependencycheck) +} + +subprojects { + group = "org.hypertrace.core.graphql" + pluginManager.withPlugin("java") { + apply(plugin = commonLibs.plugins.hypertrace.codestyle.get().pluginId) + configure { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } +} + +dependencyCheck { + format = org.owasp.dependencycheck.reporting.ReportGenerator.Format.ALL.toString() + suppressionFile = "owasp-suppressions.xml" + scanConfigurations.add("runtimeClasspath") + failBuildOnCVSS = 7.0F +} diff --git a/hypertrace-core-graphql/codecov.yml b/hypertrace-core-graphql/codecov.yml new file mode 100644 index 00000000..8a333e58 --- /dev/null +++ b/hypertrace-core-graphql/codecov.yml @@ -0,0 +1,2 @@ +codecov: + max_report_age: off diff --git a/hypertrace-core-graphql/gradle.properties b/hypertrace-core-graphql/gradle.properties new file mode 100644 index 00000000..13e3631b --- /dev/null +++ b/hypertrace-core-graphql/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.parallel=true +org.gradle.daemon=true +org.gradle.caching=true +org.gradle.configureondemand=true + diff --git a/hypertrace-core-graphql/gradle/libs.versions.toml b/hypertrace-core-graphql/gradle/libs.versions.toml new file mode 100644 index 00000000..49706fcb --- /dev/null +++ b/hypertrace-core-graphql/gradle/libs.versions.toml @@ -0,0 +1,5 @@ + +[libraries] +graphql-annotations = { module = "io.github.graphql-java:graphql-java-annotations", version = "9.1" } +graphql-servlet = { module = "com.graphql-java-kickstart:graphql-java-servlet", version = "14.0.0" } +opentelemetry-proto = { module = "io.opentelemetry:opentelemetry-proto", version = "1.1.0-alpha" } diff --git a/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.jar b/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.properties b/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e6aba251 --- /dev/null +++ b/hypertrace-core-graphql/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hypertrace-core-graphql/gradlew b/hypertrace-core-graphql/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/hypertrace-core-graphql/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hypertrace-core-graphql/gradlew.bat b/hypertrace-core-graphql/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/hypertrace-core-graphql/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hypertrace-core-graphql/helm/Chart.yaml b/hypertrace-core-graphql/helm/Chart.yaml new file mode 100644 index 00000000..6fafa00a --- /dev/null +++ b/hypertrace-core-graphql/helm/Chart.yaml @@ -0,0 +1,9 @@ +# Command to package: +# helm package --version --app-version +apiVersion: v2 +name: hypertrace-core-graphql-service +description: A Helm chart for Hypertrace Core GraphQL +type: application + +# This is the chart version. It will always be set at the command line, this value is meaningless. +version: 0.1.0 diff --git a/hypertrace-core-graphql/helm/templates/deployment.yaml b/hypertrace-core-graphql/helm/templates/deployment.yaml new file mode 100644 index 00000000..6e56eb82 --- /dev/null +++ b/hypertrace-core-graphql/helm/templates/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + labels: + release: {{ .Release.Name }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.service.adminPort }}" +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: {{ .Values.maxUnavailable }} + selector: + matchLabels: + {{- toYaml .Values.deploymentSelectorMatchLabels | nindent 6 }} + template: + metadata: + labels: + {{- toYaml .Values.podLabels | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + - name: admin-port + containerPort: {{ .Values.service.adminPort }} + env: + - name: SERVICE_NAME + value: {{ .Chart.Name }} + - name: BOOTSTRAP_CONFIG_URI + value: "file:///app/resources/configs" + - name: LOG4J_CONFIGURATION_FILE + value: "/app/log/log4j2.properties" + - name: JAVA_TOOL_OPTIONS + value: {{ .Values.javaOpts | quote }} + volumeMounts: + - name: log4j-config + mountPath: /app/log + - name: service-config + mountPath: /app/resources/configs/{{ .Chart.Name }}/application.conf + subPath: application.conf + livenessProbe: + initialDelaySeconds: {{ int .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ int .Values.livenessProbe.periodSeconds }} + tcpSocket: + port: http + readinessProbe: + initialDelaySeconds: {{ int .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ int .Values.readinessProbe.periodSeconds }} + httpGet: + path: /health + port: admin-port + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: service-config + configMap: + name: {{ .Values.serviceConfig.name }} + - name: log4j-config + configMap: + name: {{ .Values.logConfig.name }} + {{- with .Values.nodeLabels }} + nodeSelector: + {{- toYaml . | nindent 8}} + {{- end }} diff --git a/hypertrace-core-graphql/helm/templates/logconfig.yaml b/hypertrace-core-graphql/helm/templates/logconfig.yaml new file mode 100644 index 00000000..d46b0b6a --- /dev/null +++ b/hypertrace-core-graphql/helm/templates/logconfig.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.logConfig.name }} + labels: + release: {{ .Release.Name }} +data: + log4j2.properties: |- + status = error + name = PropertiesConfig + + appender.console.type = Console + appender.console.name = STDOUT + appender.console.layout.type = PatternLayout + appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c{1.} - %msg%n + + {{- if .Values.logConfig.appender.rolling.enabled }} + appender.rolling.type = RollingFile + appender.rolling.name = ROLLING_FILE + appender.rolling.fileName = ${env:SERVICE_NAME}.log + appender.rolling.filePattern = ${env:SERVICE_NAME}-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz + appender.rolling.layout.type = PatternLayout + appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c{1.} - %msg%n + appender.rolling.policies.type = Policies + appender.rolling.policies.time.type = TimeBasedTriggeringPolicy + appender.rolling.policies.time.interval = 3600 + appender.rolling.policies.time.modulate = true + appender.rolling.policies.size.type = SizeBasedTriggeringPolicy + appender.rolling.policies.size.size = 20MB + appender.rolling.strategy.type = DefaultRolloverStrategy + appender.rolling.strategy.max = 5 + {{- end }} + + rootLogger.level = {{ .Values.logConfig.rootLogger.level }} + rootLogger.appenderRef.stdout.ref = STDOUT + {{- if .Values.logConfig.appender.rolling.enabled }} + rootLogger.appenderRef.rolling.ref = ROLLING_FILE + {{- end }} diff --git a/hypertrace-core-graphql/helm/templates/service.yaml b/hypertrace-core-graphql/helm/templates/service.yaml new file mode 100644 index 00000000..ae718dfa --- /dev/null +++ b/hypertrace-core-graphql/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + release: {{ .Release.Name }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- toYaml .Values.serviceSelectorLabels | nindent 4 }} diff --git a/hypertrace-core-graphql/helm/templates/serviceconfig.yaml b/hypertrace-core-graphql/helm/templates/serviceconfig.yaml new file mode 100644 index 00000000..382369c8 --- /dev/null +++ b/hypertrace-core-graphql/helm/templates/serviceconfig.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.serviceConfig.name }} + labels: + release: {{ .Release.Name }} +data: + application.conf: |- + service.name = {{ .Chart.Name }} + service.port = {{ .Values.service.port }} + service.admin.port = {{ .Values.service.adminPort }} + + {{ if .Values.serviceConfig.defaultTenantId }} + defaultTenantId = {{ .Values.serviceConfig.defaultTenantId }} + {{ end }} + + graphql.urlPath = {{ .Values.serviceConfig.urlPath }} + graphql.corsEnabled = {{ .Values.serviceConfig.corsEnabled }} + graphql.timeout = {{ .Values.serviceConfig.timeoutDuration }} + + threads.io.max = {{ .Values.serviceConfig.threads.io }} + threads.request.max = {{ .Values.serviceConfig.threads.request }} + + attribute.service = { + host = {{ .Values.serviceConfig.attributeService.host }} + port = {{ .Values.serviceConfig.attributeService.port }} + } + + gateway.service = { + host = {{ .Values.serviceConfig.gatewayService.host }} + port = {{ .Values.serviceConfig.gatewayService.port }} + } \ No newline at end of file diff --git a/hypertrace-core-graphql/helm/values.yaml b/hypertrace-core-graphql/helm/values.yaml new file mode 100644 index 00000000..3b7debce --- /dev/null +++ b/hypertrace-core-graphql/helm/values.yaml @@ -0,0 +1,64 @@ +replicaCount: 1 +maxUnavailable: 0 + +image: + repository: hypertrace/hypertrace-core-graphql-service + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +service: + type: ClusterIP + port: 23431 + adminPort: 23432 + +podLabels: + app: hypertrace-core-graphql + +deploymentSelectorMatchLabels: + app: hypertrace-core-graphql + +serviceSelectorLabels: + app: hypertrace-core-graphql + +nodeLabels: {} + +livenessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 +readinessProbe: + initialDelaySeconds: 2 + periodSeconds: 5 + +javaOpts: "-Xms256M -Xmx512M" + +resources: + limits: + cpu: 0.5 + memory: 512Mi + requests: + cpu: 100m + memory: 512Mi + +serviceConfig: + name: hypertrace-core-graphql-service-config + urlPath: /graphql + corsEnabled: true + defaultTenantId: "" + timeoutDuration: 30s + threads: + io: 10 + request: 10 + attributeService: + host: attribute-service + port: 9012 + gatewayService: + host: gateway-service + port: 50071 +logConfig: + name: hypertrace-core-graphql-log-config + rootLogger: + level: INFO + appender: + rolling: + enabled: false diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/build.gradle.kts new file mode 100644 index 00000000..0f14d1b1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + `java-library` +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/gradle.lockfile new file mode 100644 index 00000000..c8fcb9b3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/gradle.lockfile @@ -0,0 +1,8 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeString.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeString.java new file mode 100644 index 00000000..75ca4773 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope-constants/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeString.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.atttributes.scopes; + +public interface HypertraceCoreAttributeScopeString { + String SPAN = "EVENT"; + String TRACE = "TRACE"; + String LOG_EVENT = "LOG_EVENT"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/build.gradle.kts new file mode 100644 index 00000000..227f1630 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.rxjava3) + api(projects.hypertraceCoreGraphqlAttributeStore) + api(projects.hypertraceCoreGraphqlCommonSchema) + // These are kept in a separate project so they can be referenced by other projects without circular dependencies + compileOnly(projects.hypertraceCoreGraphqlAttributeScopeConstants) +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/gradle.lockfile new file mode 100644 index 00000000..05764b1c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/gradle.lockfile @@ -0,0 +1,61 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=runtimeClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=runtimeClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScope.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScope.java new file mode 100644 index 00000000..84dd191a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScope.java @@ -0,0 +1,19 @@ +package org.hypertrace.core.graphql.atttributes.scopes; + +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; + +enum HypertraceCoreAttributeScope implements AttributeScope { + TRACE(HypertraceCoreAttributeScopeString.TRACE), + SPAN(HypertraceCoreAttributeScopeString.SPAN); + + private final String scope; + + HypertraceCoreAttributeScope(String scope) { + this.scope = scope; + } + + @Override + public String getScopeString() { + return this.scope; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeModule.java new file mode 100644 index 00000000..30e6478d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-scope/src/main/java/org/hypertrace/core/graphql/atttributes/scopes/HypertraceCoreAttributeScopeModule.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.atttributes.scopes; + +import static org.hypertrace.core.graphql.attributes.IdMapping.forForeignId; +import static org.hypertrace.core.graphql.attributes.IdMapping.forId; +import static org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString.LOG_EVENT; +import static org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString.SPAN; +import static org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString.TRACE; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.attributes.IdMapping; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; + +public class HypertraceCoreAttributeScopeModule extends AbstractModule { + + @Override + protected void configure() { + bind(Key.get(new TypeLiteral>() {})) + .toInstance(HypertraceCoreAttributeScope.class); + Multibinder idBinder = Multibinder.newSetBinder(binder(), IdMapping.class); + idBinder.addBinding().toInstance(forId(SPAN, "id")); + idBinder.addBinding().toInstance(forForeignId(SPAN, TRACE, "traceId")); + idBinder.addBinding().toInstance(forForeignId(LOG_EVENT, SPAN, "spanId")); + idBinder.addBinding().toInstance(forId(TRACE, "id")); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/build.gradle.kts new file mode 100644 index 00000000..d2f29aa2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(projects.hypertraceCoreGraphqlSpi) + api(projects.hypertraceCoreGraphqlContext) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.rxjava3) + implementation(commonLibs.guava) + + implementation(commonLibs.hypertrace.attributeservice.cachingclient) + implementation(commonLibs.hypertrace.attributeservice.api) + implementation(commonLibs.hypertrace.grpcutils.rx.client) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + implementation(projects.hypertraceCoreGraphqlRxUtils) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/gradle.lockfile new file mode 100644 index 00000000..5b6ff987 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/gradle.lockfile @@ -0,0 +1,79 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeIdentifier.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeIdentifier.java new file mode 100644 index 00000000..f4cfcffd --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeIdentifier.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.attributes; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Builder +@Accessors(fluent = true) +public class AttributeIdentifier { + String scope; + String key; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModel.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModel.java new file mode 100644 index 00000000..45e8d50a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModel.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.attributes; + +import java.util.List; + +public interface AttributeModel { + + String id(); + + String scope(); + + String key(); + + String displayName(); + + AttributeModelType type(); + + String units(); + + boolean onlySupportsGrouping(); + + boolean onlySupportsAggregation(); + + List supportedMetricAggregationTypes(); + + boolean groupable(); + + boolean isCustom(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelMetricAggregationType.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelMetricAggregationType.java new file mode 100644 index 00000000..58452285 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelMetricAggregationType.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.attributes; + +public enum AttributeModelMetricAggregationType { + COUNT, + AVG, + SUM, + MIN, + MAX, + AVGRATE, + PERCENTILE, + DISTINCT_COUNT, + DISTINCT_ARRAY +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslator.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslator.java new file mode 100644 index 00000000..a2cd57ba --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslator.java @@ -0,0 +1,118 @@ +package org.hypertrace.core.graphql.attributes; + +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_BOOL; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_BOOL_ARRAY; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_DOUBLE; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_DOUBLE_ARRAY; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_INT64; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_INT64_ARRAY; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_STRING; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_STRING_ARRAY; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_STRING_MAP; +import static org.hypertrace.core.attribute.service.v1.AttributeKind.TYPE_TIMESTAMP; + +import com.google.common.collect.ImmutableBiMap; +import java.util.List; +import java.util.Optional; +import java.util.UnknownFormatConversionException; +import java.util.stream.Collectors; +import org.hypertrace.core.attribute.service.v1.AggregateFunction; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.AttributeMetadata; +import org.hypertrace.core.attribute.service.v1.AttributeType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AttributeModelTranslator { + private static final Logger LOGGER = LoggerFactory.getLogger(AttributeModelTranslator.class); + private static final ImmutableBiMap TYPE_MAPPING = + ImmutableBiMap.builder() + .put(TYPE_STRING, AttributeModelType.STRING) + .put(TYPE_BOOL, AttributeModelType.BOOLEAN) + .put(TYPE_INT64, AttributeModelType.LONG) + .put(TYPE_DOUBLE, AttributeModelType.DOUBLE) + .put(TYPE_TIMESTAMP, AttributeModelType.TIMESTAMP) + .put(TYPE_STRING_MAP, AttributeModelType.STRING_MAP) + .put(TYPE_STRING_ARRAY, AttributeModelType.STRING_ARRAY) + .put(TYPE_DOUBLE_ARRAY, AttributeModelType.DOUBLE_ARRAY) + .put(TYPE_BOOL_ARRAY, AttributeModelType.BOOLEAN_ARRAY) + .put(TYPE_INT64_ARRAY, AttributeModelType.LONG_ARRAY) + .build(); + + public Optional translate(AttributeMetadata attributeMetadata) { + try { + return Optional.of( + DefaultAttributeModel.builder() + .id(attributeMetadata.getId()) + .scope(attributeMetadata.getScopeString()) + .key(attributeMetadata.getKey()) + .displayName(attributeMetadata.getDisplayName()) + .type(this.convertType(attributeMetadata.getValueKind())) + .units(attributeMetadata.getUnit()) + .onlySupportsGrouping(attributeMetadata.getOnlyAggregationsAllowed()) + .onlySupportsAggregation(attributeMetadata.getType().equals(AttributeType.METRIC)) + .supportedMetricAggregationTypes( + this.convertMetricAggregationTypes( + attributeMetadata.getSupportedAggregationsList())) + .groupable(attributeMetadata.getGroupable()) + .isCustom(attributeMetadata.getCustom()) + .build()); + } catch (Exception e) { + LOGGER.warn("Dropping attribute {} : {}", attributeMetadata.getId(), e.getMessage()); + return Optional.empty(); + } + } + + @SuppressWarnings("unused") + public AttributeKind convertType(AttributeModelType type) { + return Optional.ofNullable(TYPE_MAPPING.inverse().get(type)) + .orElseThrow( + () -> + new UnknownFormatConversionException( + String.format("Unrecognized attribute type %s", type.name()))); + } + + private List convertMetricAggregationTypes( + List aggregationTypes) { + return aggregationTypes.stream() + .map(this::convertMetricAggregationType) + .collect(Collectors.toUnmodifiableList()); + } + + private AttributeModelMetricAggregationType convertMetricAggregationType( + AggregateFunction aggregateFunction) { + switch (aggregateFunction) { + case COUNT: + return AttributeModelMetricAggregationType.COUNT; + case AVG: + return AttributeModelMetricAggregationType.AVG; + case SUM: + return AttributeModelMetricAggregationType.SUM; + case MIN: + return AttributeModelMetricAggregationType.MIN; + case MAX: + return AttributeModelMetricAggregationType.MAX; + case AVGRATE: + return AttributeModelMetricAggregationType.AVGRATE; + case PERCENTILE: + return AttributeModelMetricAggregationType.PERCENTILE; + case DISTINCT_COUNT: + return AttributeModelMetricAggregationType.DISTINCT_COUNT; + case DISTINCT_ARRAY: + return AttributeModelMetricAggregationType.DISTINCT_ARRAY; + case AGG_UNDEFINED: + case UNRECOGNIZED: + default: + throw new UnknownFormatConversionException( + String.format("Unrecognized aggregate function %s", aggregateFunction.name())); + } + } + + private AttributeModelType convertType(AttributeKind kind) { + return Optional.ofNullable(TYPE_MAPPING.get(kind)) + .orElseThrow( + () -> + new UnknownFormatConversionException( + String.format("Unrecognized attribute kind %s", kind.name()))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelType.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelType.java new file mode 100644 index 00000000..c7e79650 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeModelType.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.attributes; + +public enum AttributeModelType { + STRING, + BOOLEAN, + LONG, + DOUBLE, + TIMESTAMP, + STRING_MAP, + STRING_ARRAY, + DOUBLE_ARRAY, + LONG_ARRAY, + BOOLEAN_ARRAY; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStore.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStore.java new file mode 100644 index 00000000..cbdc5b04 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStore.java @@ -0,0 +1,29 @@ +package org.hypertrace.core.graphql.attributes; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import org.hypertrace.core.attribute.service.v1.AttributeMetadata; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface AttributeStore { + Single> getAllExternal(GraphQlRequestContext context); + + Single get(GraphQlRequestContext context, String scope, String key); + + Single getIdAttribute(GraphQlRequestContext context, String scope); + + Single getAttributeById(GraphQlRequestContext context, String attributeId); + + Single getForeignIdAttribute( + GraphQlRequestContext context, String scope, String foreignScope); + + Completable create(final GraphQlRequestContext context, final List attributes); + + Completable delete(final GraphQlRequestContext context, final AttributeIdentifier identifier); + + Single update( + final GraphQlRequestContext context, + final AttributeIdentifier identifier, + final AttributeUpdate update); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStoreModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStoreModule.java new file mode 100644 index 00000000..bdd80140 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeStoreModule.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.graphql.attributes; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.multibindings.Multibinder; +import io.reactivex.rxjava3.core.Scheduler; +import org.hypertrace.core.graphql.rx.BoundedIoScheduler; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; + +public class AttributeStoreModule extends AbstractModule { + + @Override + protected void configure() { + bind(AttributeStore.class).to(CachingAttributeStore.class); + Multibinder.newSetBinder(binder(), IdMapping.class); + Multibinder.newSetBinder(binder(), IdMappingLoader.class); + requireBinding(GraphQlServiceConfig.class); + requireBinding(GrpcContextBuilder.class); + requireBinding(GrpcChannelRegistry.class); + requireBinding(Key.get(Scheduler.class, BoundedIoScheduler.class)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeUpdate.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeUpdate.java new file mode 100644 index 00000000..548ca76d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/AttributeUpdate.java @@ -0,0 +1,49 @@ +package org.hypertrace.core.graphql.attributes; + +import static java.util.stream.Collectors.toUnmodifiableList; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.attribute.service.v1.Update; + +@Value +@Builder +@Accessors(fluent = true) +public class AttributeUpdate { + private static final List> MAPPER_LIST = + List.of(new UpdateMapper<>(AttributeUpdate::displayName, Update.Builder::setDisplayName)); + + @Nullable String displayName; + + List buildUpdates() { + return MAPPER_LIST.stream() + .map(mapper -> mapper.apply(this)) + .flatMap(Optional::stream) + .collect(toUnmodifiableList()); + } + + @Value + private static class UpdateMapper implements Function> { + Function valueAccessor; + BiFunction valueSetter; + + @Override + public Optional apply(final AttributeUpdate attributeUpdate) { + final T value = valueAccessor.apply(attributeUpdate); + if (Objects.isNull(value)) { + return Optional.empty(); + } + + final Update.Builder builder = Update.newBuilder(); + final Update update = valueSetter.apply(builder, value).build(); + return Optional.of(update); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/CachingAttributeStore.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/CachingAttributeStore.java new file mode 100644 index 00000000..8f7dc27d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/CachingAttributeStore.java @@ -0,0 +1,176 @@ +package org.hypertrace.core.graphql.attributes; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; +import java.time.Duration; +import java.util.List; +import java.util.NoSuchElementException; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.hypertrace.core.attribute.service.cachingclient.CachingAttributeClient; +import org.hypertrace.core.attribute.service.v1.AttributeMetadata; +import org.hypertrace.core.attribute.service.v1.AttributeMetadataFilter; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.core.grpcutils.client.rx.GrpcRxExecutionContext; + +@Singleton +class CachingAttributeStore implements AttributeStore { + private final CachingAttributeClient cachingAttributeClient; + private final IdLookup idLookup; + private final GrpcContextBuilder grpcContextBuilder; + private final AttributeModelTranslator translator; + + @Inject + CachingAttributeStore( + IdLookup idLookup, + GrpcContextBuilder grpcContextBuilder, + AttributeModelTranslator translator, + GrpcChannelRegistry channelRegistry, + GraphQlServiceConfig serviceConfig) { + this( + idLookup, + grpcContextBuilder, + translator, + CachingAttributeClient.builder( + channelRegistry.forAddress( + serviceConfig.getAttributeServiceHost(), + serviceConfig.getAttributeServicePort())) + .withCacheExpiration(Duration.ofMinutes(3)) + .withMaximumCacheContexts(1000) + .build()); + } + + CachingAttributeStore( + IdLookup idLookup, + GrpcContextBuilder grpcContextBuilder, + AttributeModelTranslator translator, + CachingAttributeClient cachingAttributeClient) { + this.idLookup = idLookup; + this.grpcContextBuilder = grpcContextBuilder; + this.translator = translator; + this.cachingAttributeClient = cachingAttributeClient; + } + + @Override + public Single> getAllExternal(GraphQlRequestContext requestContext) { + return GrpcRxExecutionContext.forContext(this.grpcContextBuilder.build(requestContext)) + .wrapSingle(this.cachingAttributeClient::getAll) + .flattenAsObservable(list -> list) + .filter(attribute -> !attribute.getInternal()) + .mapOptional(this.translator::translate) + .toList(); + } + + @Override + public Single get( + GraphQlRequestContext requestContext, String scope, String key) { + return GrpcRxExecutionContext.forContext(this.grpcContextBuilder.build(requestContext)) + .wrapSingle(() -> this.cachingAttributeClient.get(scope, key)) + .toMaybe() + .mapOptional(this.translator::translate) + .switchIfEmpty(Single.error(this.buildErrorForMissingAttribute(scope, key))); + } + + @Override + public Single getIdAttribute(GraphQlRequestContext context, String scope) { + return this.getIdKey(context, scope).flatMap(key -> this.get(context, scope, key)); + } + + @Override + public Single getForeignIdAttribute( + GraphQlRequestContext context, String scope, String foreignScope) { + return this.getForeignIdKey(context, scope, foreignScope) + .flatMap(key -> this.get(context, scope, key)); + } + + @Override + public Single getAttributeById( + GraphQlRequestContext context, String attributeId) { + return grpcContextBuilder + .build(context) + .call(() -> cachingAttributeClient.get(attributeId)) + .mapOptional(this.translator::translate) + .switchIfEmpty(Single.error(this.buildErrorForMissingAttributeId(attributeId))); + } + + @Override + public Completable create( + final GraphQlRequestContext context, final List attributes) { + return this.grpcContextBuilder + .build(context) + .call(() -> cachingAttributeClient.create(attributes)); + } + + @Override + public Completable delete( + final GraphQlRequestContext context, final AttributeIdentifier identifier) { + return this.grpcContextBuilder + .build(context) + .call(() -> cachingAttributeClient.delete(buildFilter(identifier))); + } + + @Override + public Single update( + final GraphQlRequestContext context, + final AttributeIdentifier identifier, + final AttributeUpdate update) { + final Single metadataSingle = + get(context, identifier.scope(), identifier.key()); + return metadataSingle.flatMap( + metadata -> + this.grpcContextBuilder + .build(context) + .call(() -> cachingAttributeClient.update(metadata.id(), update.buildUpdates())) + .mapOptional(translator::translate) + .switchIfEmpty( + Single.error( + this.buildErrorForMissingAttribute(identifier.scope(), identifier.key())))); + } + + private Single getForeignIdKey( + GraphQlRequestContext context, String scope, String foreignScope) { + return this.idLookup + .foreignIdKey(context, scope, foreignScope) + .switchIfEmpty( + Single.error(this.buildErrorForMissingForeignScopeMapping(scope, foreignScope))); + } + + private Single getIdKey(GraphQlRequestContext context, String scope) { + return this.idLookup + .idKey(context, scope) + .switchIfEmpty(Single.error(this.buildErrorForMissingIdMapping(scope))); + } + + private NoSuchElementException buildErrorForMissingAttribute(String scope, String key) { + return new NoSuchElementException( + String.format("No attribute available for scope '%s' and key '%s'", scope, key)); + } + + private NoSuchElementException buildErrorForMissingAttributeId(String attributeId) { + return new NoSuchElementException( + String.format("No attribute available for attribute id '%s'", attributeId)); + } + + private NoSuchElementException buildErrorForMissingForeignScopeMapping( + String scope, String foreignScope) { + return new NoSuchElementException( + String.format( + "No id attribute registered for scope '%s' and foreign scope '%s'", + scope, foreignScope)); + } + + private NoSuchElementException buildErrorForMissingIdMapping(String scope) { + return new NoSuchElementException( + String.format("No id attribute registered for scope '%s'", scope)); + } + + private AttributeMetadataFilter buildFilter(final AttributeIdentifier filter) { + return AttributeMetadataFilter.newBuilder() + .addKey(filter.key()) + .addScopeString(filter.scope()) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultAttributeModel.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultAttributeModel.java new file mode 100644 index 00000000..84c82eec --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultAttributeModel.java @@ -0,0 +1,27 @@ +package org.hypertrace.core.graphql.attributes; + +import static lombok.AccessLevel.PRIVATE; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Builder(toBuilder = true) +@Accessors(fluent = true) +@AllArgsConstructor(access = PRIVATE) +class DefaultAttributeModel implements AttributeModel { + String id; + String scope; + String key; + String displayName; + AttributeModelType type; + String units; + boolean onlySupportsGrouping; + boolean onlySupportsAggregation; + List supportedMetricAggregationTypes; + boolean groupable; + boolean isCustom; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultIdMapping.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultIdMapping.java new file mode 100644 index 00000000..21916d2f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/DefaultIdMapping.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.graphql.attributes; + +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Accessors(fluent = true) +class DefaultIdMapping implements IdMapping { + @Nonnull String containingScope; + @Nonnull String idAttribute; + @Nullable String foreignScope; + + @Override + public String foreignScope() { + return Optional.ofNullable(this.foreignScope).orElse(containingScope); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdLookup.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdLookup.java new file mode 100644 index 00000000..00d846eb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdLookup.java @@ -0,0 +1,78 @@ +package org.hypertrace.core.graphql.attributes; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Single; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.hypertrace.core.graphql.context.ContextualCachingKey; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.rx.BoundedIoScheduler; + +@Singleton +class IdLookup { + + private final LoadingCache>> + idMapCache; + private final Set mappingLoaders; + private final Scheduler boundedIoScheduler; + + @Inject + IdLookup( + Set idMappings, + Set mappingLoaders, + @BoundedIoScheduler Scheduler boundedIoScheduler) { + this.mappingLoaders = + ImmutableSet.builder() + .addAll(mappingLoaders) + .add(requestContext -> Observable.fromIterable(idMappings)) + .build(); + this.boundedIoScheduler = boundedIoScheduler; + this.idMapCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(15, TimeUnit.MINUTES) + .build(CacheLoader.from(this::loadMappingsForContext)); + } + + Maybe idKey(GraphQlRequestContext requestContext, String scope) { + return this.foreignIdKey(requestContext, scope, scope); + } + + Maybe foreignIdKey( + GraphQlRequestContext requestContext, String scope, String foreignScope) { + return this.getOrInvalidate(requestContext) + .mapOptional(table -> Optional.ofNullable(table.get(scope, foreignScope))); + } + + private Single> loadMappingsForContext( + ContextualCachingKey key) { + return Observable.fromIterable(this.mappingLoaders) + .flatMap( + idMappingLoader -> + idMappingLoader.loadMappings(key.getContext()).subscribeOn(this.boundedIoScheduler)) + // This is added to have distinct values while building the + // immutable table in case of having duplicate id definitions + .distinct() + .collect( + ImmutableTable.toImmutableTable( + IdMapping::containingScope, IdMapping::foreignScope, IdMapping::idAttribute)) + .cache(); + } + + private Single> getOrInvalidate( + GraphQlRequestContext context) { + return this.idMapCache + .getUnchecked(context.getCachingKey()) + .doOnError(x -> this.idMapCache.invalidate(context.getCachingKey())); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMapping.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMapping.java new file mode 100644 index 00000000..9e2191ac --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMapping.java @@ -0,0 +1,29 @@ +package org.hypertrace.core.graphql.attributes; + +import java.util.Objects; +import javax.annotation.Nonnull; + +public interface IdMapping { + + String containingScope(); + + String idAttribute(); + + default String foreignScope() { + return null; + } + + static IdMapping forId(@Nonnull String scope, @Nonnull String idAttribute) { + Objects.requireNonNull(scope); + Objects.requireNonNull(idAttribute); + return new DefaultIdMapping(scope, idAttribute, null); + } + + static IdMapping forForeignId( + @Nonnull String scope, @Nonnull String foreignScope, @Nonnull String idAttribute) { + Objects.requireNonNull(scope); + Objects.requireNonNull(foreignScope); + Objects.requireNonNull(idAttribute); + return new DefaultIdMapping(scope, idAttribute, foreignScope); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMappingLoader.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMappingLoader.java new file mode 100644 index 00000000..4e5962cc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/main/java/org/hypertrace/core/graphql/attributes/IdMappingLoader.java @@ -0,0 +1,8 @@ +package org.hypertrace.core.graphql.attributes; + +import io.reactivex.rxjava3.core.Observable; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface IdMappingLoader { + Observable loadMappings(GraphQlRequestContext requestContext); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslatorTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslatorTest.java new file mode 100644 index 00000000..9e066d4c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/AttributeModelTranslatorTest.java @@ -0,0 +1,117 @@ +package org.hypertrace.core.graphql.attributes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Optional; +import org.hypertrace.core.attribute.service.v1.AggregateFunction; +import org.hypertrace.core.attribute.service.v1.AttributeKind; +import org.hypertrace.core.attribute.service.v1.AttributeMetadata; +import org.hypertrace.core.attribute.service.v1.AttributeType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AttributeModelTranslatorTest { + + private AttributeModelTranslator translator; + private AttributeMetadata metadata; + private DefaultAttributeModel expectedModel; + + @BeforeEach + void beforeEach() { + this.translator = new AttributeModelTranslator(); + this.metadata = + AttributeMetadata.newBuilder() + .setId("id") + .setScopeString("TRACE") + .setKey("key") + .setDisplayName("display name") + .setValueKind(AttributeKind.TYPE_STRING) + .setType(AttributeType.ATTRIBUTE) + .setUnit("unit") + .setOnlyAggregationsAllowed(true) + .addAllSupportedAggregations(List.of(AggregateFunction.SUM, AggregateFunction.AVG)) + .setGroupable(true) + .setCustom(true) + .build(); + + this.expectedModel = + DefaultAttributeModel.builder() + .id("id") + .scope("TRACE") + .key("key") + .displayName("display name") + .type(AttributeModelType.STRING) + .units("unit") + .onlySupportsGrouping(true) + .onlySupportsAggregation(false) + .supportedMetricAggregationTypes( + List.of( + AttributeModelMetricAggregationType.SUM, + AttributeModelMetricAggregationType.AVG)) + .groupable(true) + .isCustom(true) + .build(); + } + + @Test + void canTranslateAttributeModel() { + assertEquals(Optional.of(this.expectedModel), this.translator.translate(this.metadata)); + } + + @Test + void translatesMetricstoOnlySupportAggregation() { + AttributeMetadata metricMetadata = + this.metadata.toBuilder().setType(AttributeType.METRIC).build(); + DefaultAttributeModel expectedMetricModel = + this.expectedModel.toBuilder().onlySupportsAggregation(true).build(); + assertEquals(Optional.of(expectedMetricModel), this.translator.translate(metricMetadata)); + } + + @Test + void returnsEmptyIfUnsupportedTranslation() { + AttributeMetadata unsupportedMetadata = + AttributeMetadata.newBuilder(this.metadata) + .addSupportedAggregations(AggregateFunction.AGG_UNDEFINED) + .build(); + assertEquals(Optional.empty(), this.translator.translate(unsupportedMetadata)); + } + + @Test + void testStringArrayAttributeKindTranslation() { + this.translator = new AttributeModelTranslator(); + this.metadata = + AttributeMetadata.newBuilder() + .setId("id") + .setScopeString("TRACE") + .setKey("key") + .setDisplayName("display name") + .setValueKind(AttributeKind.TYPE_STRING_ARRAY) + .setUnit("unit") + .setOnlyAggregationsAllowed(false) + .addAllSupportedAggregations( + List.of(AggregateFunction.DISTINCT_COUNT, AggregateFunction.AVGRATE)) + .setGroupable(false) + .setCustom(false) + .build(); + + this.expectedModel = + DefaultAttributeModel.builder() + .id("id") + .scope("TRACE") + .key("key") + .displayName("display name") + .type(AttributeModelType.STRING_ARRAY) + .units("unit") + .onlySupportsGrouping(false) + .supportedMetricAggregationTypes( + List.of( + AttributeModelMetricAggregationType.DISTINCT_COUNT, + AttributeModelMetricAggregationType.AVGRATE)) + .groupable(false) + .isCustom(false) + .build(); + + assertEquals(Optional.of(this.expectedModel), this.translator.translate(this.metadata)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/CachingAttributeStoreTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/CachingAttributeStoreTest.java new file mode 100644 index 00000000..dfd105d7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/CachingAttributeStoreTest.java @@ -0,0 +1,134 @@ +package org.hypertrace.core.graphql.attributes; + +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.hypertrace.core.attribute.service.cachingclient.CachingAttributeClient; +import org.hypertrace.core.attribute.service.v1.AttributeMetadata; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.core.grpcutils.context.RequestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings +class CachingAttributeStoreTest { + @Mock CachingAttributeClient mockAttributeClient; + + @Mock(answer = Answers.CALLS_REAL_METHODS) + GrpcContextBuilder mockGrpcContextBuilder; + + @Mock AttributeModelTranslator mockAttributeModeTranslator; + + @Mock IdLookup mockIdLookup; + + @Mock GraphQlRequestContext mockContext; + + private AttributeStore attributeStore; + + @BeforeEach + void beforeEach() { + this.attributeStore = + new CachingAttributeStore( + this.mockIdLookup, + this.mockGrpcContextBuilder, + this.mockAttributeModeTranslator, + this.mockAttributeClient); + } + + @Test + void supportsBasicRead() { + AttributeModel spanIdAttribute = + DefaultAttributeModel.builder().scope("SPAN").key("id").build(); + AttributeMetadata spanIdResponse = + AttributeMetadata.newBuilder() + .setScopeString("SPAN") + .setKey("id") + .setInternal(false) + .build(); + + AttributeMetadata internalAttrResponse = + AttributeMetadata.newBuilder() + .setScopeString("SPAN") + .setKey("other") + .setInternal(true) + .build(); + + when(this.mockAttributeClient.get("SPAN", "id")).thenReturn(Single.just(spanIdResponse)); + when(this.mockAttributeModeTranslator.translate(spanIdResponse)) + .thenReturn(Optional.of(spanIdAttribute)); + + assertEquals(spanIdAttribute, this.attributeStore.get(mockContext, "SPAN", "id").blockingGet()); + + when(this.mockAttributeClient.getAll()) + .thenReturn(Single.just(List.of(spanIdResponse, internalAttrResponse))); + + assertEquals( + List.of(spanIdAttribute), this.attributeStore.getAllExternal(mockContext).blockingGet()); + } + + @Test + void throwsErrorIfNoKeyMatch() { + when(this.mockAttributeClient.get(any(), any())) + .thenReturn(Single.error(new NoSuchElementException())); + + assertThrows( + NoSuchElementException.class, + this.attributeStore.get(mockContext, "SPAN", "nonExistentKey")::blockingGet); + } + + @Test + void throwsErrorForMissingIdMapping() { + when(this.mockIdLookup.idKey(any(), any())).thenReturn(Maybe.empty()); + assertThrows( + NoSuchElementException.class, + this.attributeStore.getIdAttribute(null, "SPAN")::blockingGet); + } + + @Test + void supportsForeignIdLookup() { + DefaultAttributeModel spanTraceIdAttribute = + DefaultAttributeModel.builder().scope("SPAN").key("traceId").build(); + AttributeMetadata spanTraceIdResponse = + AttributeMetadata.newBuilder().setScopeString("SPAN").setKey("traceID").build(); + + when(this.mockIdLookup.foreignIdKey(mockContext, "SPAN", "TRACE")) + .thenReturn(Maybe.just("traceId")); + when(this.mockAttributeClient.get("SPAN", "traceId")) + .thenReturn(Single.just(spanTraceIdResponse)); + when(this.mockAttributeModeTranslator.translate(spanTraceIdResponse)) + .thenReturn(Optional.of(spanTraceIdAttribute)); + + assertEquals( + spanTraceIdAttribute, + this.attributeStore.getForeignIdAttribute(mockContext, "SPAN", "TRACE").blockingGet()); + } + + @Test + void testCreate() { + final RequestContext requestContext = RequestContext.forTenantId("some-tenant"); + final Completable mockCompletable = mock(Completable.class); + when(mockGrpcContextBuilder.build(mockContext)).thenReturn(requestContext); + when(mockAttributeClient.create(emptyList())).thenReturn(mockCompletable); + + final Completable result = attributeStore.create(mockContext, emptyList()); + assertSame(mockCompletable, result); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/IdLookupTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/IdLookupTest.java new file mode 100644 index 00000000..2b22db24 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-attribute-store/src/test/java/org/hypertrace/core/graphql/attributes/IdLookupTest.java @@ -0,0 +1,111 @@ +package org.hypertrace.core.graphql.attributes; + +import static org.hypertrace.core.graphql.attributes.IdMapping.forForeignId; +import static org.hypertrace.core.graphql.attributes.IdMapping.forId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import java.util.Collections; +import java.util.Set; +import org.hypertrace.core.graphql.context.ContextualCachingKey; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.junit.jupiter.api.Test; + +class IdLookupTest { + + @Test + void canLookupIds() { + GraphQlRequestContext mockContext = this.buildNewMockContext(); + IdLookup idLookup = + new IdLookup( + Set.of( + forId("SPAN", "s-id"), + forId("TRACE", "t-id"), + forForeignId("SPAN", "TRACE", "s-t-id")), + Collections.emptySet(), + Schedulers.single()); + + assertEquals("s-id", idLookup.idKey(mockContext, "SPAN").blockingGet()); + assertEquals("t-id", idLookup.idKey(mockContext, "TRACE").blockingGet()); + assertEquals("s-t-id", idLookup.foreignIdKey(mockContext, "SPAN", "TRACE").blockingGet()); + assertTrue(idLookup.foreignIdKey(mockContext, "TRACE", "SPAN").isEmpty().blockingGet()); + } + + @Test + void supportsMultipleAsyncLoaders() { + + GraphQlRequestContext mockContext = this.buildNewMockContext(); + + IdLookup idLookup = + new IdLookup( + Set.of(forId("SCOPE_ONE", "1-id")), + Set.of( + unused -> + Observable.just( + forId("SCOPE_TWO", "2-id"), + forForeignId("SCOPE_TWO", "SCOPE_THREE", "3-id")), + unused -> Observable.just(forId("SCOPE_THREE", "3-id"))), + Schedulers.single()); + + assertEquals("1-id", idLookup.idKey(mockContext, "SCOPE_ONE").blockingGet()); + assertEquals("2-id", idLookup.idKey(mockContext, "SCOPE_TWO").blockingGet()); + assertEquals( + "3-id", idLookup.foreignIdKey(mockContext, "SCOPE_TWO", "SCOPE_THREE").blockingGet()); + assertEquals("3-id", idLookup.idKey(mockContext, "SCOPE_THREE").blockingGet()); + assertTrue(idLookup.idKey(mockContext, "SCOPE_FOUR").isEmpty().blockingGet()); + } + + @Test + void cachesLoaderResults() { + GraphQlRequestContext mockContext = this.buildNewMockContext(); + GraphQlRequestContext otherContext = this.buildNewMockContext(); + IdMappingLoader mappingLoader = mock(IdMappingLoader.class); + when(mappingLoader.loadMappings(mockContext)) + .thenReturn(Observable.just(forId("SCOPE_ONE", "1-id"))); + when(mappingLoader.loadMappings(otherContext)) + .thenReturn(Observable.just(forId("SCOPE_ONE", "1-id-other"))); + IdLookup idLookup = + new IdLookup(Collections.emptySet(), Set.of(mappingLoader), Schedulers.single()); + + assertEquals("1-id", idLookup.idKey(mockContext, "SCOPE_ONE").blockingGet()); + assertEquals("1-id-other", idLookup.idKey(otherContext, "SCOPE_ONE").blockingGet()); + assertEquals("1-id", idLookup.idKey(mockContext, "SCOPE_ONE").blockingGet()); + assertEquals("1-id-other", idLookup.idKey(otherContext, "SCOPE_ONE").blockingGet()); + verify(mappingLoader, times(1)).loadMappings(mockContext); + verify(mappingLoader, times(1)).loadMappings(otherContext); + } + + @Test + void retriesOnError() { + GraphQlRequestContext mockContext = this.buildNewMockContext(); + + IdMappingLoader mappingLoader = mock(IdMappingLoader.class); + when(mappingLoader.loadMappings(mockContext)) + .thenReturn(Observable.error(IllegalStateException::new)); + IdLookup idLookup = + new IdLookup(Collections.emptySet(), Set.of(mappingLoader), Schedulers.single()); + + assertThrows( + IllegalStateException.class, idLookup.idKey(mockContext, "SCOPE_ONE")::blockingGet); + + when(mappingLoader.loadMappings(mockContext)) + .thenReturn(Observable.just(forId("SCOPE_ONE", "1-id"))); + + assertEquals("1-id", idLookup.idKey(mockContext, "SCOPE_ONE").blockingGet()); + } + + private GraphQlRequestContext buildNewMockContext() { + ContextualCachingKey mockCachingKey = mock(ContextualCachingKey.class); + GraphQlRequestContext mockContext = mock(GraphQlRequestContext.class); + when(mockCachingKey.getContext()).thenReturn(mockContext); + when(mockContext.getCachingKey()).thenReturn(mockCachingKey); + return mockContext; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/build.gradle.kts new file mode 100644 index 00000000..168495a4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(projects.hypertraceCoreGraphqlAttributeStore) + api(projects.hypertraceCoreGraphqlContext) + api(commonLibs.rxjava3) + api(localLibs.graphql.annotations) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + compileOnly(projects.hypertraceCoreGraphqlAttributeScopeConstants) + + implementation(projects.hypertraceCoreGraphqlDeserialization) + implementation(projects.hypertraceCoreGraphqlSchemaUtils) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/gradle.lockfile new file mode 100644 index 00000000..62f4d26e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/gradle.lockfile @@ -0,0 +1,81 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=runtimeClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=runtimeClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/AttributeExpressionDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/AttributeExpressionDeserializationConfig.java new file mode 100644 index 00000000..5d6959b8 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/AttributeExpressionDeserializationConfig.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; + +class AttributeExpressionDeserializationConfig implements ArgumentDeserializationConfig { + + @Override + public String getArgumentKey() { + return AttributeExpression.ARGUMENT_NAME; + } + + @Override + public Class getArgumentSchema() { + return AttributeExpression.class; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/CommonDeserializationModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/CommonDeserializationModule.java new file mode 100644 index 00000000..f53cc00d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/CommonDeserializationModule.java @@ -0,0 +1,57 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeKeyArgument; +import org.hypertrace.core.graphql.common.schema.id.IdArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.space.SpaceArgument; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; + +public class CommonDeserializationModule extends AbstractModule { + + @Override + protected void configure() { + Multibinder deserializationConfigMultibinder = + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class); + + deserializationConfigMultibinder.addBinding().to(TimeRangeArgumentDeserializationConfig.class); + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive(IdArgument.ARGUMENT_NAME, IdArgument.class)); + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive( + LimitArgument.ARGUMENT_NAME, LimitArgument.class)); + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive( + OffsetArgument.ARGUMENT_NAME, OffsetArgument.class)); + deserializationConfigMultibinder.addBinding().to(OrderArgumentDeserializationConfig.class); + deserializationConfigMultibinder.addBinding().to(FilterArgumentDeserializationConfig.class); + + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive( + AttributeKeyArgument.ARGUMENT_NAME, AttributeKeyArgument.class)); + + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive( + SpaceArgument.ARGUMENT_NAME, SpaceArgument.class)); + deserializationConfigMultibinder + .addBinding() + .to(AttributeExpressionDeserializationConfig.class); + + requireBinding(Key.get(new TypeLiteral>() {})); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfig.java new file mode 100644 index 00000000..87a42d0a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfig.java @@ -0,0 +1,71 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.util.List; +import javax.inject.Inject; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; + +class FilterArgumentDeserializationConfig implements ArgumentDeserializationConfig { + + private final Class attributeScopeImplementationClass; + + @Inject + FilterArgumentDeserializationConfig( + Class attributeScopeImplementationClass) { + this.attributeScopeImplementationClass = attributeScopeImplementationClass; + } + + @Override + public String getArgumentKey() { + return FilterArgument.ARGUMENT_NAME; + } + + @Override + public Class getArgumentSchema() { + return FilterArgument.class; + } + + @Override + public List jacksonModules() { + return List.of( + new SimpleModule() + .addAbstractTypeMapping(FilterArgument.class, DefaultFilterArgument.class) + .addAbstractTypeMapping(AttributeScope.class, this.attributeScopeImplementationClass)); + } + + @Value + @Accessors(fluent = true) + @NoArgsConstructor(force = true) + private static class DefaultFilterArgument implements FilterArgument { + @JsonProperty(FILTER_ARGUMENT_TYPE) + FilterType type; + + @JsonProperty(FILTER_ARGUMENT_KEY) + String key; + + @JsonProperty(FILTER_ARGUMENT_KEY_EXPRESSION) + AttributeExpression keyExpression; + + @JsonProperty(FILTER_ARGUMENT_OPERATOR) + FilterOperatorType operator; + + @JsonProperty(FILTER_ARGUMENT_VALUE) + Object value; + + @JsonProperty(FILTER_ARGUMENT_ID_TYPE) + AttributeScope idType; + + @JsonProperty(FILTER_ARGUMENT_ID_SCOPE) + String idScope; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfig.java new file mode 100644 index 00000000..5fd92587 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfig.java @@ -0,0 +1,46 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.util.List; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderDirection; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; + +class OrderArgumentDeserializationConfig implements ArgumentDeserializationConfig { + + @Override + public String getArgumentKey() { + return OrderArgument.ARGUMENT_NAME; + } + + @Override + public Class getArgumentSchema() { + return OrderArgument.class; + } + + @Override + public List jacksonModules() { + return List.of( + new SimpleModule().addAbstractTypeMapping(OrderArgument.class, DefaultOrderArgument.class)); + } + + @Value + @Accessors(fluent = true) + @NoArgsConstructor(force = true) + private static class DefaultOrderArgument implements OrderArgument { + @JsonProperty(ORDER_DIRECTION_NAME) + OrderDirection direction = OrderDirection.DESC; + + @JsonProperty(ORDER_KEY_NAME) + String key; + + @JsonProperty(ORDER_KEY_EXPRESSION_NAME) + AttributeExpression keyExpression; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfig.java new file mode 100644 index 00000000..c42f8e31 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfig.java @@ -0,0 +1,42 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.time.Instant; +import java.util.List; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; + +class TimeRangeArgumentDeserializationConfig implements ArgumentDeserializationConfig { + + @Override + public String getArgumentKey() { + return TimeRangeArgument.ARGUMENT_NAME; + } + + @Override + public Class getArgumentSchema() { + return TimeRangeArgument.class; + } + + @Override + public List jacksonModules() { + return List.of( + new SimpleModule().addAbstractTypeMapping(TimeRangeArgument.class, DefaultTimeRange.class)); + } + + @Value + @Accessors(fluent = true) + @NoArgsConstructor(force = true) + private static class DefaultTimeRange implements TimeRangeArgument { + @JsonProperty(TIME_RANGE_ARGUMENT_START_TIME) + Instant startTime; + + @JsonProperty(TIME_RANGE_ARGUMENT_END_TIME) + Instant endTime; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/fetcher/InjectableDataFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/fetcher/InjectableDataFetcher.java new file mode 100644 index 00000000..89561de5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/fetcher/InjectableDataFetcher.java @@ -0,0 +1,34 @@ +package org.hypertrace.core.graphql.common.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public abstract class InjectableDataFetcher implements DataFetcher> { + + private final Class>> dataFetcherClass; + @Nullable private DataFetcher> dataFetcherInstance; + + protected InjectableDataFetcher( + Class>> dataFetcherClass) { + this.dataFetcherClass = dataFetcherClass; + } + + @Override + public final CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + return this.getOrCreateImplementation(contextFromEnvironment(environment)).get(environment); + } + + private DataFetcher> getOrCreateImplementation( + GraphQlRequestContext context) { + if (this.dataFetcherInstance == null) { + this.dataFetcherInstance = context.constructDataFetcher(this.dataFetcherClass); + } + + return this.dataFetcherInstance; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeAssociation.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeAssociation.java new file mode 100644 index 00000000..ea0d0b10 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeAssociation.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.common.request; + +import org.hypertrace.core.graphql.attributes.AttributeModel; + +public interface AttributeAssociation { + + AttributeModel attribute(); + + T value(); + + static AttributeAssociation of(AttributeModel attribute, T value) { + return new DefaultAttributeAssociation<>(attribute, value); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequest.java new file mode 100644 index 00000000..0f1c61e1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequest.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.common.request; + +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; + +public interface AttributeRequest { + AttributeAssociation attributeExpressionAssociation(); + + default String asMapKey() { + return attributeExpressionAssociation().value().asAlias(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequestBuilder.java new file mode 100644 index 00000000..37712d0f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/AttributeRequestBuilder.java @@ -0,0 +1,37 @@ +package org.hypertrace.core.graphql.common.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.stream.Stream; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface AttributeRequestBuilder { + + Single buildForId(GraphQlRequestContext context, String attributeModelScope); + + Observable buildForAttributeQueryableSelectionSet( + GraphQlRequestContext context, + String attributeScope, + DataFetchingFieldSelectionSet attributeQueryableSelectionSet); + + Observable buildForAttributeQueryableFields( + GraphQlRequestContext context, + String attributeModelScope, + Stream attributeQueryableFields); + + Observable buildForAttributeQueryableFieldsAndId( + GraphQlRequestContext context, + String attributeScope, + Stream attributeQueryableFields); + + Single buildForAttributeExpression( + GraphQlRequestContext context, + String attributeScope, + AttributeExpression attributeExpression); + + AttributeRequest buildForAttribute(AttributeModel attribute); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/CommonRequestModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/CommonRequestModule.java new file mode 100644 index 00000000..6093584e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/CommonRequestModule.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.graphql.common.request; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; + +public class CommonRequestModule extends AbstractModule { + + @Override + protected void configure() { + bind(ResultSetRequestBuilder.class).to(DefaultResultSetRequestBuilder.class); + bind(AttributeRequestBuilder.class).to(DefaultAttributeRequestBuilder.class); + bind(FilterRequestBuilder.class).to(DefaultFilterRequestBuilder.class); + bind(ContextualRequestBuilder.class).to(SimpleContextualRequestBuilder.class); + requireBinding(AttributeStore.class); + requireBinding(ArgumentDeserializer.class); + requireBinding(AttributeAssociator.class); + requireBinding(GraphQlSelectionFinder.class); + requireBinding(AttributeScopeStringTranslator.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequest.java new file mode 100644 index 00000000..d3c0eec4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequest.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.request; + +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface ContextualRequest { + GraphQlRequestContext context(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequestBuilder.java new file mode 100644 index 00000000..3ac131bf --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ContextualRequestBuilder.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.request; + +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface ContextualRequestBuilder { + ContextualRequest build(GraphQlRequestContext context); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeAssociation.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeAssociation.java new file mode 100644 index 00000000..44f7a6d9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeAssociation.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.common.request; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeModel; + +@Value +@Accessors(fluent = true) +class DefaultAttributeAssociation implements AttributeAssociation { + AttributeModel attribute; + T value; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilder.java new file mode 100644 index 00000000..f956ec1a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilder.java @@ -0,0 +1,122 @@ +package org.hypertrace.core.graphql.common.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.stream.Stream; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeQueryable; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeKeyArgument; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +class DefaultAttributeRequestBuilder implements AttributeRequestBuilder { + + private final AttributeStore attributeStore; + private final AttributeAssociator attributeAssociator; + private final ArgumentDeserializer argumentDeserializer; + private final GraphQlSelectionFinder selectionFinder; + + @Inject + DefaultAttributeRequestBuilder( + AttributeStore attributeStore, + AttributeAssociator attributeAssociator, + ArgumentDeserializer argumentDeserializer, + GraphQlSelectionFinder selectionFinder) { + this.attributeStore = attributeStore; + this.attributeAssociator = attributeAssociator; + this.argumentDeserializer = argumentDeserializer; + this.selectionFinder = selectionFinder; + } + + @Override + public Single buildForId(GraphQlRequestContext context, String attributeScope) { + return this.attributeStore.getIdAttribute(context, attributeScope).map(this::buildForAttribute); + } + + @Override + public Observable buildForAttributeQueryableSelectionSet( + GraphQlRequestContext context, + String attributeScope, + DataFetchingFieldSelectionSet attributeQueryableSelectionSet) { + return Observable.fromStream( + this.getAttributeExpressionsForAttributeQueryableSelectionSet( + attributeQueryableSelectionSet)) + .flatMapSingle( + expression -> this.buildForAttributeExpression(context, attributeScope, expression)) + .distinct(); + } + + @Override + public Observable buildForAttributeQueryableFields( + GraphQlRequestContext context, + String attributeScope, + Stream attributeQueryableFields) { + return Observable.fromStream(attributeQueryableFields) + .map(SelectedField::getSelectionSet) + .flatMap( + selectionSet -> + this.buildForAttributeQueryableSelectionSet(context, attributeScope, selectionSet)) + .distinct(); + } + + @Override + public Observable buildForAttributeQueryableFieldsAndId( + GraphQlRequestContext context, + String attributeScope, + Stream attributeQueryableFields) { + return this.buildForAttributeQueryableFields(context, attributeScope, attributeQueryableFields) + .mergeWith(this.buildForId(context, attributeScope)) + .distinct(); + } + + @Override + public Single buildForAttributeExpression( + GraphQlRequestContext context, + String attributeScope, + AttributeExpression attributeExpression) { + return this.attributeAssociator + .associateAttribute(context, attributeScope, attributeExpression, attributeExpression.key()) + .map(DefaultAttributeRequest::new); + } + + @Override + public AttributeRequest buildForAttribute(AttributeModel attribute) { + return new DefaultAttributeRequest( + AttributeAssociation.of(attribute, AttributeExpression.forAttributeKey(attribute.key()))); + } + + private Stream getAttributeExpressionsForAttributeQueryableSelectionSet( + DataFetchingFieldSelectionSet selectionSet) { + return this.selectionFinder + .findSelections( + selectionSet, SelectionQuery.namedChild(AttributeQueryable.ATTRIBUTE_FIELD_NAME)) + .flatMap(this::resolveAttributeExpression); + } + + private Stream resolveAttributeExpression(SelectedField attributeField) { + return this.argumentDeserializer + .deserializeObject(attributeField.getArguments(), AttributeExpression.class) + .or( + () -> + this.argumentDeserializer + .deserializePrimitive(attributeField.getArguments(), AttributeKeyArgument.class) + .map(AttributeExpression::forAttributeKey)) + .stream(); + } + + @Value + @Accessors(fluent = true) + static class DefaultAttributeRequest implements AttributeRequest { + AttributeAssociation attributeExpressionAssociation; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilder.java new file mode 100644 index 00000000..cc0976e2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilder.java @@ -0,0 +1,103 @@ +package org.hypertrace.core.graphql.common.request; + +import static java.util.Objects.requireNonNull; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public class DefaultFilterRequestBuilder implements FilterRequestBuilder { + + private final AttributeAssociator attributeAssociator; + private final AttributeStore attributeStore; + private final AttributeScopeStringTranslator scopeTranslator; + + @Inject + DefaultFilterRequestBuilder( + AttributeAssociator attributeAssociator, + AttributeStore attributeStore, + AttributeScopeStringTranslator scopeTranslator) { + + this.attributeAssociator = attributeAssociator; + this.attributeStore = attributeStore; + this.scopeTranslator = scopeTranslator; + } + + @Override + public Single>> build( + GraphQlRequestContext requestContext, + String scope, + Collection filterArguments) { + return Observable.fromIterable(filterArguments) + .flatMapSingle(filter -> this.normalize(requestContext, scope, filter)) + .collect(Collectors.toUnmodifiableList()); + } + + private Single> normalize( + GraphQlRequestContext requestContext, String scope, FilterArgument filterArgument) { + switch (filterArgument.type()) { + case ATTRIBUTE: + AttributeExpression attributeExpression = + Optional.ofNullable(filterArgument.keyExpression()) + .orElseGet( + () -> + AttributeExpression.forAttributeKey(requireNonNull(filterArgument.key()))); + return this.attributeAssociator.associateAttribute( + requestContext, + scope, + new NormalizedFilter( + attributeExpression, filterArgument.operator(), filterArgument.value()), + attributeExpression.key()); + case ID: + return Maybe.fromOptional(Optional.ofNullable(filterArgument.idType())) + .map(AttributeScope::getScopeString) + .switchIfEmpty(Maybe.fromOptional(Optional.ofNullable(filterArgument.idScope()))) + .switchIfEmpty( + Single.error(new UnsupportedOperationException("ID filter must specify idScope"))) + .map(this.scopeTranslator::fromExternal) + .flatMap( + foreignScope -> + this.attributeStore.getForeignIdAttribute(requestContext, scope, foreignScope)) + .map( + foreignIdAttribute -> + AttributeAssociation.of( + foreignIdAttribute, + new NormalizedFilter( + AttributeExpression.forAttributeKey(foreignIdAttribute.key()), + filterArgument.operator(), + filterArgument.value()))); + default: + return Single.error( + new UnsupportedOperationException( + "Unrecognized filter type: " + filterArgument.type())); + } + } + + @Value + @Accessors(fluent = true) + private static class NormalizedFilter implements FilterArgument { + FilterType type = FilterType.ATTRIBUTE; + String key = null; + AttributeExpression keyExpression; + FilterOperatorType operator; + Object value; + String idScope = null; + AttributeScope idType = null; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilder.java new file mode 100644 index 00000000..fc9b3b43 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilder.java @@ -0,0 +1,257 @@ +package org.hypertrace.core.graphql.common.request; + +import static io.reactivex.rxjava3.core.Single.zip; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.space.SpaceArgument; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +class DefaultResultSetRequestBuilder implements ResultSetRequestBuilder { + private final ArgumentDeserializer argumentDeserializer; + private final GraphQlSelectionFinder selectionFinder; + private final AttributeRequestBuilder attributeRequestBuilder; + private final AttributeAssociator attributeAssociator; + private final FilterRequestBuilder filterRequestBuilder; + private static final int DEFAULT_LIMIT = 100; + private static final int DEFAULT_OFFSET = 0; + + @Inject + DefaultResultSetRequestBuilder( + ArgumentDeserializer argumentDeserializer, + GraphQlSelectionFinder selectionFinder, + AttributeRequestBuilder attributeRequestBuilder, + AttributeAssociator attributeAssociator, + FilterRequestBuilder filterRequestBuilder) { + this.argumentDeserializer = argumentDeserializer; + this.selectionFinder = selectionFinder; + this.attributeRequestBuilder = attributeRequestBuilder; + this.attributeAssociator = attributeAssociator; + this.filterRequestBuilder = filterRequestBuilder; + } + + @Override + public Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + DataFetchingFieldSelectionSet selectionSet) { + return this.build(context, requestScope, arguments, selectionSet, OrderArgument.class); + } + + @Override + public Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + DataFetchingFieldSelectionSet selectionSet, + Class orderArgumentClass) { + int limit = + this.argumentDeserializer + .deserializePrimitive(arguments, LimitArgument.class) + .orElse(DEFAULT_LIMIT); + + int offset = + this.argumentDeserializer + .deserializePrimitive(arguments, OffsetArgument.class) + .orElse(DEFAULT_OFFSET); + + TimeRangeArgument timeRange = + this.argumentDeserializer + .deserializeObject(arguments, TimeRangeArgument.class) + .orElseThrow(); + + List requestedOrders = + this.argumentDeserializer + .deserializeObjectList(arguments, orderArgumentClass) + .orElse(Collections.emptyList()); + + List requestedFilters = + this.argumentDeserializer + .deserializeObjectList(arguments, FilterArgument.class) + .orElse(Collections.emptyList()); + + Optional spaceId = + this.argumentDeserializer.deserializePrimitive(arguments, SpaceArgument.class); + + return zip( + this.attributeAssociator + .associateAttributes( + context, + requestScope, + requestedOrders, + arg -> arg.resolvedKeyExpression().key()) + .collect(Collectors.toUnmodifiableList()), + this.filterRequestBuilder.build(context, requestScope, requestedFilters), + (orders, filters) -> + this.build( + context, + requestScope, + limit, + offset, + timeRange, + orders, + filters, + this.getAttributeQueryableFields(selectionSet), + spaceId)) + .flatMap(single -> single); + } + + @Override + public Single> build( + GraphQlRequestContext context, + String requestScope, + int limit, + int offset, + TimeRangeArgument timeRange, + List> orderArguments, + Collection> filterArguments, + Stream attributeQueryableFields, + Optional spaceId) { + return zip( + this.attributeRequestBuilder + .buildForAttributeQueryableFieldsAndId(context, requestScope, attributeQueryableFields) + .collect(Collectors.toUnmodifiableSet()), + this.attributeRequestBuilder.buildForId(context, requestScope), + (attributeRequests, idAttribute) -> + new DefaultResultSetRequest<>( + context, + attributeRequests, + idAttribute, + timeRange, + limit, + offset, + orderArguments, + filterArguments, + spaceId)); + } + + @Override + public Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + List attributeExpressions) { + int limit = + this.argumentDeserializer + .deserializePrimitive(arguments, LimitArgument.class) + .orElse(DEFAULT_LIMIT); + + TimeRangeArgument timeRange = + this.argumentDeserializer + .deserializeObject(arguments, TimeRangeArgument.class) + .orElseThrow(); + + List requestedFilters = + this.argumentDeserializer + .deserializeObjectList(arguments, FilterArgument.class) + .orElse(Collections.emptyList()); + + return zip( + this.getAttributeRequests(context, requestScope, attributeExpressions) + .collect(Collectors.toList()), + this.attributeRequestBuilder.buildForId(context, requestScope), + this.filterRequestBuilder.build(context, requestScope, requestedFilters), + (attributeRequests, idAttribute, filters) -> + new DefaultResultSetRequest<>( + context, + attributeRequests, + idAttribute, + timeRange, + limit, + 0, + List.of(), + filters, + Optional.empty())); + } + + @Override + public Single> rebuildWithAdditionalFilters( + ResultSetRequest originalRequest, Collection additionalFilters) { + return this.mergeFilterLists( + originalRequest.context(), + originalRequest.idAttribute().attributeExpressionAssociation().attribute().scope(), + originalRequest.filterArguments(), + additionalFilters) + .map( + mergedFilters -> + new DefaultResultSetRequest<>( + originalRequest.context(), + originalRequest.attributes(), + originalRequest.idAttribute(), + originalRequest.timeRange(), + originalRequest.limit(), + originalRequest.offset(), + originalRequest.orderArguments(), + mergedFilters, + originalRequest.spaceId())); + } + + private Single>> mergeFilterLists( + GraphQlRequestContext requestContext, + String scope, + Collection> original, + Collection additional) { + return this.filterRequestBuilder + .build(requestContext, scope, additional) + .flattenAsObservable(list -> list) + .concatWith(Observable.fromIterable(original)) + .toList(); + } + + private Observable getAttributeRequests( + GraphQlRequestContext context, + String requestScope, + List attributeExpressions) { + return Observable.fromIterable(attributeExpressions) + .distinct() + .flatMapSingle( + attributeExpression -> + this.attributeRequestBuilder.buildForAttributeExpression( + context, requestScope, attributeExpression)); + } + + private Stream getAttributeQueryableFields( + DataFetchingFieldSelectionSet selectionSet) { + return this.selectionFinder.findSelections( + selectionSet, SelectionQuery.namedChild(ResultSet.RESULT_SET_RESULTS_NAME)); + } + + @Value + @Accessors(fluent = true) + private static class DefaultResultSetRequest + implements ResultSetRequest { + GraphQlRequestContext context; + Collection attributes; + AttributeRequest idAttribute; + TimeRangeArgument timeRange; + int limit; + int offset; + List> orderArguments; + Collection> filterArguments; + Optional spaceId; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/FilterRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/FilterRequestBuilder.java new file mode 100644 index 00000000..6908e587 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/FilterRequestBuilder.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.common.request; + +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface FilterRequestBuilder { + Single>> build( + GraphQlRequestContext requestContext, + String scope, + Collection filterArguments); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequest.java new file mode 100644 index 00000000..46418045 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequest.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.common.request; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; + +public interface ResultSetRequest extends ContextualRequest { + + // All attributes to fetch, including ID + Collection attributes(); + + TimeRangeArgument timeRange(); + + AttributeRequest idAttribute(); + + int limit(); + + int offset(); + + List> orderArguments(); + + Collection> filterArguments(); + + Optional spaceId(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequestBuilder.java new file mode 100644 index 00000000..3a6ea9bd --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/ResultSetRequestBuilder.java @@ -0,0 +1,50 @@ +package org.hypertrace.core.graphql.common.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface ResultSetRequestBuilder { + Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + DataFetchingFieldSelectionSet selectionSet); + + Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + DataFetchingFieldSelectionSet selectionSet, + Class orderArgumentClass); + + Single> build( + GraphQlRequestContext context, + String requestScope, + int limit, + int offset, + TimeRangeArgument timeRange, + List> orderArguments, + Collection> filterArguments, + Stream attributeQueryableFields, + Optional spaceId); + + Single> rebuildWithAdditionalFilters( + ResultSetRequest originalRequest, Collection additionalFilters); + + Single> build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + List attributeExpressions); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/SimpleContextualRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/SimpleContextualRequestBuilder.java new file mode 100644 index 00000000..8fc488b1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/request/SimpleContextualRequestBuilder.java @@ -0,0 +1,19 @@ +package org.hypertrace.core.graphql.common.request; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +class SimpleContextualRequestBuilder implements ContextualRequestBuilder { + + @Override + public ContextualRequest build(GraphQlRequestContext context) { + return new SimpleContextualRequest(context); + } + + @Value + @Accessors(fluent = true) + private static class SimpleContextualRequest implements ContextualRequest { + GraphQlRequestContext context; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaFragment.java new file mode 100644 index 00000000..82b2dc31 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaFragment.java @@ -0,0 +1,51 @@ +package org.hypertrace.core.graphql.common.schema; + +import graphql.annotations.processor.typeFunctions.TypeFunction; +import java.util.List; +import javax.annotation.Nonnull; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.schema.typefunctions.AttributeScopeDynamicEnum; +import org.hypertrace.core.graphql.common.schema.typefunctions.DateTimeScalar; +import org.hypertrace.core.graphql.common.schema.typefunctions.DurationScalar; +import org.hypertrace.core.graphql.common.schema.typefunctions.OffsetTimeScalar; +import org.hypertrace.core.graphql.common.schema.typefunctions.UnknownScalar; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +class CommonSchemaFragment implements GraphQlSchemaFragment { + + private final DateTimeScalar dateTimeScalar; + private final UnknownScalar unknownScalar; + private final AttributeScopeDynamicEnum attributeScopeDynamicEnum; + private final DurationScalar durationScalar; + private final OffsetTimeScalar offsetTimeScalar; + + @Inject + CommonSchemaFragment( + DateTimeScalar dateTimeScalar, + UnknownScalar unknownScalar, + AttributeScopeDynamicEnum attributeScopeDynamicEnum, + DurationScalar durationScalar, + OffsetTimeScalar offsetTimeScalar) { + this.dateTimeScalar = dateTimeScalar; + this.unknownScalar = unknownScalar; + this.attributeScopeDynamicEnum = attributeScopeDynamicEnum; + this.durationScalar = durationScalar; + this.offsetTimeScalar = offsetTimeScalar; + } + + @Override + public String fragmentName() { + return "Common Schema"; + } + + @Nonnull + @Override + public List typeFunctions() { + return List.of( + this.unknownScalar, + this.dateTimeScalar, + this.attributeScopeDynamicEnum, + this.durationScalar, + this.offsetTimeScalar); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaModule.java new file mode 100644 index 00000000..3f26baba --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/CommonSchemaModule.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.graphql.common.schema; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.common.deserialization.CommonDeserializationModule; +import org.hypertrace.core.graphql.common.request.CommonRequestModule; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class CommonSchemaModule extends AbstractModule { + + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), GraphQlSchemaFragment.class) + .addBinding() + .to(CommonSchemaFragment.class); + + install(new CommonDeserializationModule()); + install(new CommonRequestModule()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/arguments/TimeRangeArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/arguments/TimeRangeArgument.java new file mode 100644 index 00000000..b04668a3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/arguments/TimeRangeArgument.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.graphql.common.schema.arguments; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.time.Instant; + +@GraphQLName(TimeRangeArgument.TYPE_NAME) +public interface TimeRangeArgument { + String TYPE_NAME = "TimeRange"; + String ARGUMENT_NAME = "between"; // TODO rename to time range + String TIME_RANGE_ARGUMENT_START_TIME = "startTime"; + String TIME_RANGE_ARGUMENT_END_TIME = "endTime"; + + @GraphQLField + @GraphQLName(TIME_RANGE_ARGUMENT_START_TIME) + @GraphQLNonNull + Instant startTime(); + + @GraphQLField + @GraphQLName(TIME_RANGE_ARGUMENT_END_TIME) + @GraphQLNonNull + Instant endTime(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeQueryable.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeQueryable.java new file mode 100644 index 00000000..277752e5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeQueryable.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.common.schema.attributes; + +import static java.util.Objects.requireNonNull; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import java.util.Optional; +import javax.annotation.Nullable; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeKeyArgument; + +public interface AttributeQueryable { + + String ATTRIBUTE_FIELD_NAME = "attribute"; + + @GraphQLField + @GraphQLName(ATTRIBUTE_FIELD_NAME) + default Object attribute( + @Deprecated @GraphQLName(AttributeKeyArgument.ARGUMENT_NAME) @Nullable String key, + @GraphQLName(AttributeExpression.ARGUMENT_NAME) @Nullable AttributeExpression expression) { + return attribute( + Optional.ofNullable(expression) + .orElseGet(() -> AttributeExpression.forAttributeKey(requireNonNull(key)))); + } + + // Once callers are migrated off using the string, we'll remove it and use this api only + Object attribute(@GraphQLName(AttributeExpression.ARGUMENT_NAME) AttributeExpression expression); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeScope.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeScope.java new file mode 100644 index 00000000..2b38062c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeScope.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.common.schema.attributes; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(AttributeScope.TYPE_NAME) +public interface AttributeScope { + // Should be provided for the specific attributes available + String TYPE_NAME = "AttributeScope"; + + // Temporary measure until the scope enum is removed entirely + String getScopeString(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeType.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeType.java new file mode 100644 index 00000000..c494d8ef --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/AttributeType.java @@ -0,0 +1,19 @@ +package org.hypertrace.core.graphql.common.schema.attributes; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(AttributeType.TYPE_NAME) +public enum AttributeType { + STRING, + BOOLEAN, + LONG, + DOUBLE, + TIMESTAMP, + STRING_MAP, + STRING_ARRAY, + DOUBLE_ARRAY, + LONG_ARRAY, + BOOLEAN_ARRAY; + + public static final String TYPE_NAME = "AttributeType"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/MetricAggregationType.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/MetricAggregationType.java new file mode 100644 index 00000000..c40aeab0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/MetricAggregationType.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.common.schema.attributes; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(MetricAggregationType.TYPE_NAME) +public enum MetricAggregationType { + COUNT, + AVG, + SUM, + MIN, + MAX, + AVGRATE, + PERCENTILE, + DISTINCTCOUNT, + DISTINCT_ARRAY; + + public static final String TYPE_NAME = "MetricAggregationType"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeExpression.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeExpression.java new file mode 100644 index 00000000..6cae4f39 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeExpression.java @@ -0,0 +1,56 @@ +package org.hypertrace.core.graphql.common.schema.attributes.arguments; + +import com.fasterxml.jackson.annotation.JsonProperty; +import graphql.annotations.annotationTypes.GraphQLConstructor; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Accessors(fluent = true) +@GraphQLName(AttributeExpression.TYPE_NAME) +public class AttributeExpression { + public static final String ARGUMENT_NAME = "expression"; + static final String TYPE_NAME = "AttributeExpression"; + + private static final String ATTRIBUTE_KEY = "key"; + private static final String SUBPATH = "subpath"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_KEY) + @JsonProperty(ATTRIBUTE_KEY) + String key; + + @GraphQLField + @GraphQLName(SUBPATH) + @JsonProperty(SUBPATH) + Optional subpath; + + @GraphQLConstructor + public AttributeExpression( + @GraphQLName(ATTRIBUTE_KEY) String key, @GraphQLName(SUBPATH) @Nullable String subpath) { + this.key = key; + this.subpath = Optional.ofNullable(subpath); + } + + private AttributeExpression() { + this.key = null; + this.subpath = Optional.empty(); + } + + public String asAlias() { + return subpath() + .map(subpath -> String.format("%s.%s", this.key(), subpath)) + .orElseGet(this::key); + } + + public static AttributeExpression forAttributeKey(@Nonnull String key) { + return new AttributeExpression(key, null); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeKeyArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeKeyArgument.java new file mode 100644 index 00000000..0121215f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/attributes/arguments/AttributeKeyArgument.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.schema.attributes.arguments; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface AttributeKeyArgument extends PrimitiveArgument { + String ARGUMENT_NAME = "key"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/IdArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/IdArgument.java new file mode 100644 index 00000000..3a46ca2b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/IdArgument.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.schema.id; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface IdArgument extends PrimitiveArgument { + String ARGUMENT_NAME = "id"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/Identifiable.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/Identifiable.java new file mode 100644 index 00000000..2822e97f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/id/Identifiable.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.common.schema.id; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; + +public interface Identifiable { + + String IDENTITY_FIELD_NAME = "id"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(IDENTITY_FIELD_NAME) + String id(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/operator/LogicalOperator.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/operator/LogicalOperator.java new file mode 100644 index 00000000..53c1f45d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/operator/LogicalOperator.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.common.schema.operator; + +import graphql.annotations.annotationTypes.GraphQLDescription; +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLDescription("Logical operator describing how to combine multiple independent clauses") +@GraphQLName(LogicalOperator.TYPE_NAME) +public enum LogicalOperator { + AND, + OR; + static final String TYPE_NAME = "LogicalOperator"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/pair/KeyValuePair.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/pair/KeyValuePair.java new file mode 100644 index 00000000..182e52cb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/pair/KeyValuePair.java @@ -0,0 +1,27 @@ +package org.hypertrace.core.graphql.common.schema.pair; + +import graphql.annotations.annotationTypes.GraphQLDescription; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; + +@GraphQLName(KeyValuePair.TYPE_NAME) +@GraphQLDescription("A pair of a key and a value") +public interface KeyValuePair { + String TYPE_NAME = "KeyValuePair"; + + String KEY = "key"; + String VALUE = "value"; + + @GraphQLField + @GraphQLName(KEY) + @GraphQLNonNull + @GraphQLDescription("String based key name") + String key(); + + @GraphQLField + @GraphQLName(VALUE) + @GraphQLNonNull + @GraphQLDescription("Value of a generic type") + Object value(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/ResultSet.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/ResultSet.java new file mode 100644 index 00000000..0dfc82fe --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/ResultSet.java @@ -0,0 +1,34 @@ +package org.hypertrace.core.graphql.common.schema.results; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; + +public interface ResultSet { + String RESULT_SET_RESULTS_NAME = "results"; + String RESULT_SET_COUNT_NAME = "count"; + String RESULT_SET_TOTAL_NAME = "total"; + + /** + * Must be annotated in extending interface. Annotations lib doesn't handle generics, which + * requires a redeclaration and overrides these annotations. + * + *
+   *     \@GraphQLField
+   *     \@GraphQLNonNull
+   *     \@GraphQLName(RESULT_SET_RESULTS_NAME)
+   * 
+ */ + List results(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULT_SET_COUNT_NAME) + long count(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULT_SET_TOTAL_NAME) + long total(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterArgument.java new file mode 100644 index 00000000..fcf1e3c0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterArgument.java @@ -0,0 +1,60 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.filter; + +import graphql.annotations.annotationTypes.GraphQLDeprecate; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import javax.annotation.Nullable; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; + +@GraphQLName(FilterArgument.TYPE_NAME) +public interface FilterArgument { + String TYPE_NAME = "Filter"; + String ARGUMENT_NAME = "filterBy"; + String FILTER_ARGUMENT_TYPE = "type"; + String FILTER_ARGUMENT_KEY = "key"; + String FILTER_ARGUMENT_KEY_EXPRESSION = "keyExpression"; + String FILTER_ARGUMENT_OPERATOR = "operator"; + String FILTER_ARGUMENT_VALUE = "value"; + @Deprecated String FILTER_ARGUMENT_ID_TYPE = "idType"; + String FILTER_ARGUMENT_ID_SCOPE = "idScope"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(FILTER_ARGUMENT_TYPE) + FilterType type(); + + @GraphQLField + @GraphQLName(FILTER_ARGUMENT_KEY) + @Nullable + @GraphQLDeprecate + @Deprecated + String key(); + + @GraphQLField + @GraphQLName(FILTER_ARGUMENT_KEY_EXPRESSION) + @Nullable + AttributeExpression keyExpression(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(FILTER_ARGUMENT_OPERATOR) + FilterOperatorType operator(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(FILTER_ARGUMENT_VALUE) + Object value(); + + @GraphQLField + @GraphQLName(FILTER_ARGUMENT_ID_TYPE) + @Nullable + @GraphQLDeprecate + AttributeScope idType(); + + @GraphQLField + @GraphQLName(FILTER_ARGUMENT_ID_SCOPE) + @Nullable + String idScope(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterOperatorType.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterOperatorType.java new file mode 100644 index 00000000..ba257cba --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterOperatorType.java @@ -0,0 +1,22 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.filter; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(FilterOperatorType.TYPE_NAME) +public enum FilterOperatorType { + EQUALS, + NOT_EQUALS, + LESS_THAN, + LESS_THAN_OR_EQUAL_TO, + GREATER_THAN, + GREATER_THAN_OR_EQUAL_TO, + LIKE, + IN, + NOT_IN, + CONTAINS_KEY, + CONTAINS_KEY_VALUE, + CONTAINS_KEY_LIKE, + NOT_CONTAINS_KEY; + + public static final String TYPE_NAME = "FilterOperatorType"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterType.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterType.java new file mode 100644 index 00000000..f16c227d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/filter/FilterType.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.filter; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(FilterType.TYPE_NAME) +public enum FilterType { + ATTRIBUTE, + ID; + + public static final String TYPE_NAME = "FilterType"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderArgument.java new file mode 100644 index 00000000..74dcb951 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderArgument.java @@ -0,0 +1,42 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.order; + +import static java.util.Objects.requireNonNull; +import static org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression.forAttributeKey; + +import graphql.annotations.annotationTypes.GraphQLDeprecate; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import java.util.Optional; +import javax.annotation.Nullable; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; + +@GraphQLName(OrderArgument.TYPE_NAME) +public interface OrderArgument { + String ARGUMENT_NAME = "orderBy"; // TODO rename to order + String TYPE_NAME = "Order"; + String ORDER_KEY_NAME = "key"; + String ORDER_KEY_EXPRESSION_NAME = "keyExpression"; + String ORDER_DIRECTION_NAME = "direction"; + + @GraphQLField + @Nullable + @GraphQLName(ORDER_KEY_NAME) + @Deprecated + @GraphQLDeprecate + String key(); + + @GraphQLField + @GraphQLName(ORDER_KEY_EXPRESSION_NAME) + @Nullable + AttributeExpression keyExpression(); + + @GraphQLField + @GraphQLName(ORDER_DIRECTION_NAME) + OrderDirection direction(); + + // Temporary way of getting the expression from either keyExpression or key field + default AttributeExpression resolvedKeyExpression() { + return Optional.ofNullable(keyExpression()) + .orElseGet(() -> forAttributeKey(requireNonNull(key()))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderDirection.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderDirection.java new file mode 100644 index 00000000..a15e09c0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/order/OrderDirection.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.order; + +import graphql.annotations.annotationTypes.GraphQLName; + +@GraphQLName(OrderDirection.TYPE_NAME) +public enum OrderDirection { + ASC, + DESC; + + public static final String TYPE_NAME = "OrderDirection"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/LimitArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/LimitArgument.java new file mode 100644 index 00000000..f05fdd65 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/LimitArgument.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.page; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface LimitArgument extends PrimitiveArgument { + String ARGUMENT_NAME = "limit"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/OffsetArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/OffsetArgument.java new file mode 100644 index 00000000..f41ede76 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/page/OffsetArgument.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.page; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface OffsetArgument extends PrimitiveArgument { + String ARGUMENT_NAME = "offset"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/space/SpaceArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/space/SpaceArgument.java new file mode 100644 index 00000000..4337ad3c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/results/arguments/space/SpaceArgument.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.schema.results.arguments.space; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface SpaceArgument extends PrimitiveArgument { + String ARGUMENT_NAME = "space"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/time/TimeUnit.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/time/TimeUnit.java new file mode 100644 index 00000000..5be3079d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/time/TimeUnit.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.graphql.common.schema.time; + +import java.time.temporal.ChronoUnit; + +public enum TimeUnit { + MILLISECONDS(ChronoUnit.MILLIS), + SECONDS(ChronoUnit.SECONDS), + MINUTES(ChronoUnit.MINUTES), + HOURS(ChronoUnit.HOURS), + DAYS(ChronoUnit.DAYS); + + private final ChronoUnit chronoUnit; + + TimeUnit(ChronoUnit chronoUnit) { + this.chronoUnit = chronoUnit; + } + + public ChronoUnit getChronoUnit() { + return chronoUnit; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/type/Typed.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/type/Typed.java new file mode 100644 index 00000000..5de19c18 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/type/Typed.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.common.schema.type; + +public interface Typed { + String TYPE_FIELD_NAME = "type"; + + /** + * Must be annotated in extending interface. Annotations lib doesn't handle generics, which + * requires a redeclaration and overrides these annotations. + * + *
+   *     \@GraphQLField
+   *     \@GraphQLNonNull
+   *     \@GraphQLName(TYPE_FIELD_NAME)
+   * 
+ */ + T type(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/AttributeScopeDynamicEnum.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/AttributeScopeDynamicEnum.java new file mode 100644 index 00000000..44b8e037 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/AttributeScopeDynamicEnum.java @@ -0,0 +1,39 @@ +package org.hypertrace.core.graphql.common.schema.typefunctions; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.retrievers.GraphQLObjectInfoRetriever; +import graphql.annotations.processor.typeBuilders.EnumBuilder; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLType; +import java.lang.reflect.AnnotatedType; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; + +public class AttributeScopeDynamicEnum implements TypeFunction { + + private final GraphQLEnumType enumType; + + @Inject + AttributeScopeDynamicEnum(Class attributeScopeImplementationClass) { + this.enumType = + new EnumBuilder(new GraphQLObjectInfoRetriever()) + .getEnumBuilder(attributeScopeImplementationClass) + .name(AttributeScope.TYPE_NAME) + .build(); + } + + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return aClass == AttributeScope.class; + } + + @Override + public GraphQLType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return this.enumType; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DateTimeScalar.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DateTimeScalar.java new file mode 100644 index 00000000..3b60ebaf --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DateTimeScalar.java @@ -0,0 +1,83 @@ +package org.hypertrace.core.graphql.common.schema.typefunctions; + +import graphql.GraphqlErrorException; +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.function.Function; + +public class DateTimeScalar implements TypeFunction { + + private static final GraphQLScalarType DATE_TIME_SCALAR = + GraphQLScalarType.newScalar() + .name("DateTime") + .description("An ISO-8601 formatted DateTime Scalar") + .coercing( + new Coercing() { + @Override + public String serialize(Object fetcherResult) throws CoercingSerializeException { + return toInstant(fetcherResult, CoercingSerializeException::new).toString(); + } + + @Override + public Instant parseValue(Object input) throws CoercingParseValueException { + return toInstant(input, CoercingParseValueException::new); + } + + @Override + public Instant parseLiteral(Object input) throws CoercingParseLiteralException { + return toInstant(input, CoercingParseLiteralException::new); + } + + private Instant toInstant( + Object instantInput, Function errorWrapper) throws E { + try { + if (instantInput instanceof TemporalAccessor) { + return Instant.from((TemporalAccessor) instantInput); + } + if (instantInput instanceof CharSequence) { + return parse((CharSequence) instantInput); + } + if (instantInput instanceof StringValue) { + return parse(((StringValue) instantInput).getValue()); + } + } catch (DateTimeException exception) { + throw errorWrapper.apply(exception); + } + throw errorWrapper.apply( + new DateTimeException( + String.format( + "Cannot convert provided format '%s' to Instant", + instantInput.getClass().getCanonicalName()))); + } + }) + .build(); + + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return Instant.class.isAssignableFrom(aClass); + } + + @Override + public GraphQLScalarType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return DATE_TIME_SCALAR; + } + + private static Instant parse(CharSequence charSequence) { + return OffsetDateTime.parse(charSequence).toInstant(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DurationScalar.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DurationScalar.java new file mode 100644 index 00000000..add1b037 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/DurationScalar.java @@ -0,0 +1,77 @@ +package org.hypertrace.core.graphql.common.schema.typefunctions; + +import graphql.GraphqlErrorException; +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.DateTimeException; +import java.time.Duration; +import java.util.function.Function; + +public class DurationScalar implements TypeFunction { + + private static final GraphQLScalarType DURATION_SCALAR = + GraphQLScalarType.newScalar() + .name("Duration") + .description("An ISO-8601 formatted Duration Scalar") + .coercing( + new Coercing() { + @Override + public String serialize(Object fetcherResult) throws CoercingSerializeException { + return toDuration(fetcherResult, CoercingSerializeException::new).toString(); + } + + @Override + public Duration parseValue(Object input) throws CoercingParseValueException { + return toDuration(input, CoercingParseValueException::new); + } + + @Override + public Duration parseLiteral(Object input) throws CoercingParseLiteralException { + return toDuration(input, CoercingParseLiteralException::new); + } + + private Duration toDuration( + Object durationInput, Function errorWrapper) throws E { + try { + if (durationInput instanceof Duration) { + return Duration.from((Duration) durationInput); + } + if (durationInput instanceof CharSequence) { + return Duration.parse((CharSequence) durationInput); + } + if (durationInput instanceof StringValue) { + return Duration.parse(((StringValue) durationInput).getValue()); + } + } catch (Exception exception) { + throw errorWrapper.apply(exception); + } + throw errorWrapper.apply( + new DateTimeException( + String.format( + "Cannot convert provided format '%s' to Duration", + durationInput.getClass().getCanonicalName()))); + } + }) + .build(); + + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return Duration.class.isAssignableFrom(aClass); + } + + @Override + public GraphQLScalarType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return DURATION_SCALAR; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/OffsetTimeScalar.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/OffsetTimeScalar.java new file mode 100644 index 00000000..cd035920 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/OffsetTimeScalar.java @@ -0,0 +1,78 @@ +package org.hypertrace.core.graphql.common.schema.typefunctions; + +import graphql.GraphqlErrorException; +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.DateTimeException; +import java.time.OffsetTime; +import java.time.temporal.TemporalAccessor; +import java.util.function.Function; + +public class OffsetTimeScalar implements TypeFunction { + + private static final GraphQLScalarType OFFSET_TIME_SCALAR = + GraphQLScalarType.newScalar() + .name("OffsetTime") + .description("An ISO-8601 formatted OffsetTime Scalar") + .coercing( + new Coercing() { + @Override + public String serialize(Object fetcherResult) throws CoercingSerializeException { + return toOffsetTime(fetcherResult, CoercingSerializeException::new).toString(); + } + + @Override + public OffsetTime parseValue(Object input) throws CoercingParseValueException { + return toOffsetTime(input, CoercingParseValueException::new); + } + + @Override + public OffsetTime parseLiteral(Object input) throws CoercingParseLiteralException { + return toOffsetTime(input, CoercingParseLiteralException::new); + } + + private OffsetTime toOffsetTime( + Object offsetInput, Function errorWrapper) throws E { + try { + if (offsetInput instanceof TemporalAccessor) { + return OffsetTime.from((TemporalAccessor) offsetInput); + } + if (offsetInput instanceof CharSequence) { + return OffsetTime.parse((CharSequence) offsetInput); + } + if (offsetInput instanceof StringValue) { + return OffsetTime.parse(((StringValue) offsetInput).getValue()); + } + } catch (DateTimeException exception) { + throw errorWrapper.apply(exception); + } + throw errorWrapper.apply( + new DateTimeException( + String.format( + "Cannot convert provided format '%s' to OffsetTime", + offsetInput.getClass().getCanonicalName()))); + } + }) + .build(); + + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return OffsetTime.class.isAssignableFrom(aClass); + } + + @Override + public GraphQLScalarType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return OFFSET_TIME_SCALAR; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/UnknownScalar.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/UnknownScalar.java new file mode 100644 index 00000000..ec331e0b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/schema/typefunctions/UnknownScalar.java @@ -0,0 +1,106 @@ +package org.hypertrace.core.graphql.common.schema.typefunctions; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.language.ArrayValue; +import graphql.language.BooleanValue; +import graphql.language.EnumValue; +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.Instant; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class UnknownScalar implements TypeFunction { + + private static final GraphQLScalarType ATTRIBUTE_VALUE_SCALAR = + GraphQLScalarType.newScalar() + .name("Unknown") + .description( + "A value of unknown type: A string, int, float, boolean, array, enum or object") + .coercing( + new Coercing<>() { + @Override + public Object serialize(Object fetcherResult) throws CoercingSerializeException { + if (fetcherResult instanceof Instant) { + // todo handle for other direction as well + return fetcherResult.toString(); + } + // Use default serializer + return fetcherResult; + } + + @Override + public Object parseValue(Object input) { + return input; + } + + @Override + public Object parseLiteral(Object input) throws CoercingParseLiteralException { + return this.parseFromAst(input, CoercingParseLiteralException::new); + } + + private Object parseFromAst( + Object input, Function errorWrapper) throws E { + Function recurse = + value -> this.parseFromAst(value, errorWrapper); + + if (input instanceof StringValue) { + return ((StringValue) input).getValue(); + } + if (input instanceof IntValue) { + return ((IntValue) input).getValue(); + } + if (input instanceof FloatValue) { + return ((FloatValue) input).getValue(); + } + if (input instanceof BooleanValue) { + return ((BooleanValue) input).isValue(); + } + if (input instanceof ArrayValue) { + return ((ArrayValue) input) + .getValues().stream().map(recurse).collect(Collectors.toUnmodifiableList()); + } + if (input instanceof EnumValue) { + return ((EnumValue) input).getName(); + } + if (input instanceof ObjectValue) { + return ((ObjectValue) input) + .getObjectFields().stream() + .collect( + Collectors.toUnmodifiableMap( + ObjectField::getName, + field -> recurse.apply(field.getValue()))); + } + + throw errorWrapper.apply( + new IllegalArgumentException( + String.format( + "Unsupported input of type %s", + input.getClass().getCanonicalName()))); + } + }) + .build(); + + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return Object.class == aClass; + } + + @Override + public GraphQLScalarType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return ATTRIBUTE_VALUE_SCALAR; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/BiConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/BiConverter.java new file mode 100644 index 00000000..ce40591b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/BiConverter.java @@ -0,0 +1,16 @@ +package org.hypertrace.core.graphql.common.utils; + +import io.reactivex.rxjava3.core.Single; + +/** + * A Function, similar to {@link java.util.function.BiFunction} that accepts two arguments and + * converts them to a result asynchronously. + * + * @param + * @param + * @param + */ +public interface BiConverter { + + Single convert(F1 f1, F2 f2); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/CollectorUtils.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/CollectorUtils.java new file mode 100644 index 00000000..751b57f8 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/CollectorUtils.java @@ -0,0 +1,31 @@ +package org.hypertrace.core.graphql.common.utils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +public class CollectorUtils { + + public static Collector, ?, Map> immutableMapEntryCollector() { + return Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue); + } + + public static Collector> flatten( + Function> collectionMapper) { + return flatten(collectionMapper, Collectors.toUnmodifiableList()); + } + + public static > Collector flatten( + Function> collectionMapper, Collector downstream) { + return Collectors.flatMapping(collectionMapper.andThen(Collection::stream), downstream); + } + + public static Collector firstOrDefault(T defaultValue) { + return Collectors.collectingAndThen( + Collectors.toSet(), set -> set.stream().findFirst().orElse(defaultValue)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/Converter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/Converter.java new file mode 100644 index 00000000..a0d30eea --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/Converter.java @@ -0,0 +1,8 @@ +package org.hypertrace.core.graphql.common.utils; + +import io.reactivex.rxjava3.core.Single; + +public interface Converter { + + Single convert(F from); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/TriConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/TriConverter.java new file mode 100644 index 00000000..678f4693 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/TriConverter.java @@ -0,0 +1,7 @@ +package org.hypertrace.core.graphql.common.utils; + +import io.reactivex.rxjava3.core.Single; + +public interface TriConverter { + Single convert(F1 f1, F2 f2, F3 f3); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeAssociator.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeAssociator.java new file mode 100644 index 00000000..bcdf3b08 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeAssociator.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.function.Function; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface AttributeAssociator { + + Observable> associateAttributes( + GraphQlRequestContext context, + String requestScope, + Collection inputs, + Function attributeKeyMapper); + + Single> associateAttribute( + GraphQlRequestContext context, String requestScope, T input, String attributeKey); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeModelMetricAggregationTypeConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeModelMetricAggregationTypeConverter.java new file mode 100644 index 00000000..61190fee --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeModelMetricAggregationTypeConverter.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import io.reactivex.rxjava3.core.Single; +import java.util.UnknownFormatConversionException; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; + +class AttributeModelMetricAggregationTypeConverter + implements Converter { + + @Override + public Single convert( + MetricAggregationType aggregationType) { + switch (aggregationType) { + case COUNT: + return Single.just(AttributeModelMetricAggregationType.COUNT); + case AVG: + return Single.just(AttributeModelMetricAggregationType.AVG); + case SUM: + return Single.just(AttributeModelMetricAggregationType.SUM); + case MIN: + return Single.just(AttributeModelMetricAggregationType.MIN); + case MAX: + return Single.just(AttributeModelMetricAggregationType.MAX); + case AVGRATE: + return Single.just(AttributeModelMetricAggregationType.AVGRATE); + case PERCENTILE: + return Single.just(AttributeModelMetricAggregationType.PERCENTILE); + case DISTINCTCOUNT: + return Single.just(AttributeModelMetricAggregationType.DISTINCT_COUNT); + case DISTINCT_ARRAY: + return Single.just(AttributeModelMetricAggregationType.DISTINCT_ARRAY); + default: + return Single.error( + new UnknownFormatConversionException( + String.format( + "Unrecognized attribute metric aggregation type %s", aggregationType.name()))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeScopeStringTranslator.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeScopeStringTranslator.java new file mode 100644 index 00000000..5a582965 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeScopeStringTranslator.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +/** + * This serves as a temporary bridge to keep the external names of scopes unchanged until the + * internal names can be changed to align. + */ +public interface AttributeScopeStringTranslator { + + String fromExternal(String external); + + String toExternal(String internal); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverter.java new file mode 100644 index 00000000..af209e94 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverter.java @@ -0,0 +1,57 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import static org.hypertrace.core.graphql.attributes.AttributeModelType.BOOLEAN; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.BOOLEAN_ARRAY; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.DOUBLE; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.DOUBLE_ARRAY; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.LONG; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.LONG_ARRAY; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.STRING; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.STRING_ARRAY; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.STRING_MAP; +import static org.hypertrace.core.graphql.attributes.AttributeModelType.TIMESTAMP; + +import com.google.common.collect.ImmutableBiMap; +import io.reactivex.rxjava3.core.Single; +import java.util.Optional; +import java.util.UnknownFormatConversionException; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.utils.Converter; + +public class AttributeTypeConverter implements Converter { + private static final ImmutableBiMap TYPE_MAPPING = + ImmutableBiMap.builder() + .put(STRING, AttributeType.STRING) + .put(BOOLEAN, AttributeType.BOOLEAN) + .put(LONG, AttributeType.LONG) + .put(DOUBLE, AttributeType.DOUBLE) + .put(TIMESTAMP, AttributeType.TIMESTAMP) + .put(STRING_MAP, AttributeType.STRING_MAP) + .put(STRING_ARRAY, AttributeType.STRING_ARRAY) + .put(DOUBLE_ARRAY, AttributeType.DOUBLE_ARRAY) + .put(LONG_ARRAY, AttributeType.LONG_ARRAY) + .put(BOOLEAN_ARRAY, AttributeType.BOOLEAN_ARRAY) + .build(); + + @Override + public Single convert(AttributeModelType type) { + return Optional.ofNullable(TYPE_MAPPING.get(type)) + .map(Single::just) + .orElseGet( + () -> + Single.error( + new UnknownFormatConversionException( + String.format("Unrecognized attribute type %s", type.name())))); + } + + public Single convert(final AttributeType type) { + return Optional.ofNullable(TYPE_MAPPING.inverse().get(type)) + .map(Single::just) + .orElseGet( + () -> + Single.error( + new UnknownFormatConversionException( + String.format("Unrecognized attribute type %s", type.name())))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeUtilsModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeUtilsModule.java new file mode 100644 index 00000000..1f436fc7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeUtilsModule.java @@ -0,0 +1,33 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; + +public class AttributeUtilsModule extends AbstractModule { + + @Override + protected void configure() { + bind(Key.get(new TypeLiteral>() {})) + .to(AttributeTypeConverter.class); + bind(Key.get( + new TypeLiteral< + Converter>() {})) + .to(MetricAggregationTypeConverter.class); + bind(Key.get( + new TypeLiteral< + Converter>() {})) + .to(AttributeModelMetricAggregationTypeConverter.class); + + bind(AttributeAssociator.class).to(DefaultAttributeAssociator.class); + bind(AttributeScopeStringTranslator.class).to(DefaultAttributeScopeStringTranslator.class); + + requireBinding(AttributeStore.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeAssociator.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeAssociator.java new file mode 100644 index 00000000..fd2dcd16 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeAssociator.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.function.Function; +import javax.inject.Inject; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +class DefaultAttributeAssociator implements AttributeAssociator { + + private final AttributeStore attributeStore; + + @Inject + DefaultAttributeAssociator(AttributeStore attributeStore) { + this.attributeStore = attributeStore; + } + + @Override + public Observable> associateAttributes( + GraphQlRequestContext context, + String requestScope, + Collection inputs, + Function attributeKeyMapper) { + return Observable.fromIterable(inputs) + .flatMapSingle( + input -> + this.associateAttribute( + context, requestScope, input, attributeKeyMapper.apply(input))); + } + + @Override + public Single> associateAttribute( + GraphQlRequestContext context, String requestScope, T input, String attributeKey) { + return this.attributeStore + .get(context, requestScope, attributeKey) + .map(attribute -> AttributeAssociation.of(attribute, input)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeScopeStringTranslator.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeScopeStringTranslator.java new file mode 100644 index 00000000..5f0c2a83 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/DefaultAttributeScopeStringTranslator.java @@ -0,0 +1,23 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; + +class DefaultAttributeScopeStringTranslator implements AttributeScopeStringTranslator { + private static final String SPAN_SCOPE_EXTERNAL_NAME = "SPAN"; + + @Override + public String fromExternal(String external) { + if (SPAN_SCOPE_EXTERNAL_NAME.equals(external)) { + return HypertraceCoreAttributeScopeString.SPAN; + } + return external; + } + + @Override + public String toExternal(String internal) { + if (HypertraceCoreAttributeScopeString.SPAN.equals(internal)) { + return SPAN_SCOPE_EXTERNAL_NAME; + } + return internal; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/MetricAggregationTypeConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/MetricAggregationTypeConverter.java new file mode 100644 index 00000000..4112e04a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/main/java/org/hypertrace/core/graphql/common/utils/attributes/MetricAggregationTypeConverter.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import io.reactivex.rxjava3.core.Single; +import java.util.UnknownFormatConversionException; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; + +class MetricAggregationTypeConverter + implements Converter { + + @Override + public Single convert( + AttributeModelMetricAggregationType aggregationType) { + switch (aggregationType) { + case COUNT: + return Single.just(MetricAggregationType.COUNT); + case AVG: + return Single.just(MetricAggregationType.AVG); + case SUM: + return Single.just(MetricAggregationType.SUM); + case MIN: + return Single.just(MetricAggregationType.MIN); + case MAX: + return Single.just(MetricAggregationType.MAX); + case AVGRATE: + return Single.just(MetricAggregationType.AVGRATE); + case PERCENTILE: + return Single.just(MetricAggregationType.PERCENTILE); + case DISTINCT_COUNT: + return Single.just(MetricAggregationType.DISTINCTCOUNT); + case DISTINCT_ARRAY: + return Single.just(MetricAggregationType.DISTINCT_ARRAY); + default: + return Single.error( + new UnknownFormatConversionException( + String.format( + "Unrecognized attribute metric aggregation type %s", aggregationType.name()))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfigTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfigTest.java new file mode 100644 index 00000000..f4461393 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/FilterArgumentDeserializationConfigTest.java @@ -0,0 +1,82 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.deserialization.GraphQlDeserializationRegistryModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FilterArgumentDeserializationConfigTest { + private ArgumentDeserializer argumentDeserializer; + + private enum TestAttributeScope implements AttributeScope { + SCOPE; + + @Override + public String getScopeString() { + return "SCOPE"; + } + } + + @BeforeEach + void beforeEach() { + this.argumentDeserializer = + Guice.createInjector( + new GraphQlDeserializationRegistryModule(), + new AbstractModule() { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class) + .addBinding() + .to(FilterArgumentDeserializationConfig.class); + bind(Key.get(new TypeLiteral>() {})) + .toInstance(TestAttributeScope.class); + } + }) + .getInstance(ArgumentDeserializer.class); + } + + @Test + void deserializesValueIfPresent() { + Map argMap = + Map.of( + FilterArgument.ARGUMENT_NAME, + List.of( + Map.of( + FilterArgument.FILTER_ARGUMENT_KEY, + "fooKey", + FilterArgument.FILTER_ARGUMENT_OPERATOR, + FilterOperatorType.EQUALS, + FilterArgument.FILTER_ARGUMENT_TYPE, + FilterType.ATTRIBUTE, + FilterArgument.FILTER_ARGUMENT_VALUE, + "fooValue", + FilterArgument.FILTER_ARGUMENT_ID_TYPE, + TestAttributeScope.SCOPE))); + + List result = + this.argumentDeserializer.deserializeObjectList(argMap, FilterArgument.class).orElseThrow(); + assertEquals(1, result.size()); + assertEquals("fooKey", result.get(0).key()); + assertEquals(FilterOperatorType.EQUALS, result.get(0).operator()); + assertEquals(FilterType.ATTRIBUTE, result.get(0).type()); + assertEquals("fooValue", result.get(0).value()); + assertEquals(TestAttributeScope.SCOPE, result.get(0).idType()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfigTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfigTest.java new file mode 100644 index 00000000..bf7e3261 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/OrderArgumentDeserializationConfigTest.java @@ -0,0 +1,65 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.multibindings.Multibinder; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderDirection; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.deserialization.GraphQlDeserializationRegistryModule; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OrderArgumentDeserializationConfigTest { + private ArgumentDeserializer argumentDeserializer; + + @BeforeEach + void beforeEach() { + this.argumentDeserializer = + Guice.createInjector( + new GraphQlDeserializationRegistryModule(), + new AbstractModule() { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class) + .addBinding() + .to(OrderArgumentDeserializationConfig.class); + } + }) + .getInstance(ArgumentDeserializer.class); + } + + @Test + void deserializesValueIfPresent() { + Map argMap = + Map.of( + OrderArgument.ARGUMENT_NAME, + List.of( + Map.of( + OrderArgument.ORDER_KEY_NAME, + "fooKey", + OrderArgument.ORDER_DIRECTION_NAME, + OrderDirection.ASC), + Map.of(OrderArgument.ORDER_KEY_NAME, "barKey"))); + + List result = + this.argumentDeserializer.deserializeObjectList(argMap, OrderArgument.class).orElseThrow(); + assertEquals(2, result.size()); + assertEquals( + AttributeExpression.forAttributeKey("fooKey"), result.get(0).resolvedKeyExpression()); + Assertions.assertEquals(OrderDirection.ASC, result.get(0).direction()); + assertEquals( + AttributeExpression.forAttributeKey("barKey"), result.get(1).resolvedKeyExpression()); + Assertions.assertEquals(OrderDirection.DESC, result.get(1).direction()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfigTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfigTest.java new file mode 100644 index 00000000..8abaf58b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/deserialization/TimeRangeArgumentDeserializationConfigTest.java @@ -0,0 +1,58 @@ +package org.hypertrace.core.graphql.common.deserialization; + +import static org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument.ARGUMENT_NAME; +import static org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument.TIME_RANGE_ARGUMENT_END_TIME; +import static org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument.TIME_RANGE_ARGUMENT_START_TIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.multibindings.Multibinder; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.deserialization.GraphQlDeserializationRegistryModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TimeRangeArgumentDeserializationConfigTest { + private ArgumentDeserializer argumentDeserializer; + + @BeforeEach + void beforeEach() { + this.argumentDeserializer = + Guice.createInjector( + new GraphQlDeserializationRegistryModule(), + new AbstractModule() { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class) + .addBinding() + .to(TimeRangeArgumentDeserializationConfig.class); + } + }) + .getInstance(ArgumentDeserializer.class); + } + + @Test + void deserializesValueIfPresent() { + Instant endTime = Instant.now(); + Instant startTime = endTime.minus(1, ChronoUnit.HOURS); + Map argMap = + Map.of( + ARGUMENT_NAME, + Map.of( + TIME_RANGE_ARGUMENT_START_TIME, startTime, TIME_RANGE_ARGUMENT_END_TIME, endTime)); + + TimeRangeArgument result = + this.argumentDeserializer.deserializeObject(argMap, TimeRangeArgument.class).orElseThrow(); + assertEquals(startTime, result.startTime()); + assertEquals(endTime, result.endTime()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilderTest.java new file mode 100644 index 00000000..034460b9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultAttributeRequestBuilderTest.java @@ -0,0 +1,75 @@ +package org.hypertrace.core.graphql.common.request; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeKeyArgument; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultAttributeRequestBuilderTest { + @Mock AttributeStore mockAttributeStore; + @Mock AttributeAssociator mockAttributeAssociator; + @Mock ArgumentDeserializer mockArgumentDeserializer; + @Mock GraphQlSelectionFinder mockSelectionFinder; + @Mock GraphQlRequestContext mockContext; + @Mock DataFetchingFieldSelectionSet mockSelectionSet; + @Mock AttributeModel mockAttribute; + @Mock SelectedField mockSelectedField; + + private DefaultAttributeRequestBuilder requestBuilder; + + @BeforeEach + void beforeEach() { + this.requestBuilder = + new DefaultAttributeRequestBuilder( + mockAttributeStore, + mockAttributeAssociator, + mockArgumentDeserializer, + mockSelectionFinder); + } + + @Test + void canBuildRequestForSelectionSet() { + AttributeAssociation expectedResultExpression = + AttributeAssociation.of(this.mockAttribute, AttributeExpression.forAttributeKey("fooKey")); + when(this.mockSelectionFinder.findSelections(eq(this.mockSelectionSet), any())) + .thenReturn(Stream.of(this.mockSelectedField)); + when(this.mockArgumentDeserializer.deserializePrimitive(any(), eq(AttributeKeyArgument.class))) + .thenReturn(Optional.of("fooKey")); + when(this.mockAttributeAssociator.associateAttribute( + eq(this.mockContext), + eq("SPAN"), + eq(AttributeExpression.forAttributeKey("fooKey")), + eq("fooKey"))) + .thenReturn(Single.just(expectedResultExpression)); + + List returned = + this.requestBuilder + .buildForAttributeQueryableSelectionSet(this.mockContext, "SPAN", this.mockSelectionSet) + .toList() + .blockingGet(); + + assertEquals(1, returned.size()); + assertEquals(expectedResultExpression, returned.get(0).attributeExpressionAssociation()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilderTest.java new file mode 100644 index 00000000..26eab961 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultFilterRequestBuilderTest.java @@ -0,0 +1,154 @@ +package org.hypertrace.core.graphql.common.request; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultFilterRequestBuilderTest { + @Mock AttributeAssociator mockAttributeAssociator; + @Mock AttributeStore mockAttributeStore; + @Mock GraphQlRequestContext mockRequestContext; + @Mock AttributeScopeStringTranslator mockScopeStringTranslator; + + private DefaultFilterRequestBuilder requestBuilder; + + @BeforeEach + void beforeEach() { + this.requestBuilder = + new DefaultFilterRequestBuilder( + mockAttributeAssociator, mockAttributeStore, mockScopeStringTranslator); + } + + @Test + void canBuildIdFilterWithEnumScope() { + final FilterOperatorType filterOperatorType = FilterOperatorType.LESS_THAN; + final long filterValue = 42; + final FilterType filterType = FilterType.ID; + final String filterAttributeKey = "filterKey"; + final String idScopeString = "foreign_scope"; + final String currentScope = "primary_scope"; + + final AttributeScope idScope = mock(AttributeScope.class); + when(idScope.getScopeString()).thenReturn(idScopeString); + when(this.mockScopeStringTranslator.fromExternal(idScopeString)).thenReturn(idScopeString); + final FilterArgument filterArgument = mock(FilterArgument.class); + when(filterArgument.type()).thenReturn(filterType); + when(filterArgument.operator()).thenReturn(filterOperatorType); + when(filterArgument.value()).thenReturn(filterValue); + when(filterArgument.idType()).thenReturn(idScope); + + final AttributeModel mockForeignAttribute = mock(AttributeModel.class); + when(mockForeignAttribute.key()).thenReturn(filterAttributeKey); + + when(this.mockAttributeStore.getForeignIdAttribute( + mockRequestContext, currentScope, idScopeString)) + .thenReturn(Single.just(mockForeignAttribute)); + + AttributeAssociation builtFilter = + this.requestBuilder + .build(this.mockRequestContext, currentScope, List.of(filterArgument)) + .blockingGet() + .get(0); + + assertEquals(mockForeignAttribute, builtFilter.attribute()); + assertEquals( + AttributeExpression.forAttributeKey(filterAttributeKey), + builtFilter.value().keyExpression()); + assertEquals(filterOperatorType, builtFilter.value().operator()); + assertEquals(filterValue, builtFilter.value().value()); + assertEquals(FilterType.ATTRIBUTE, builtFilter.value().type()); + } + + @Test + void canBuildIdFilterWithStringScope() { + final FilterOperatorType filterOperatorType = FilterOperatorType.LESS_THAN; + final long filterValue = 42; + final FilterType filterType = FilterType.ID; + final String filterAttributeKey = "filterKey"; + final String idScope = "foreign_scope"; + final String idScopeExternal = "foreign_scope_external"; + final String currentScope = "primary_scope"; + + final FilterArgument filterArgument = mock(FilterArgument.class); + when(filterArgument.type()).thenReturn(filterType); + when(filterArgument.operator()).thenReturn(filterOperatorType); + when(filterArgument.value()).thenReturn(filterValue); + when(filterArgument.idScope()).thenReturn(idScopeExternal); + + final AttributeModel mockForeignAttribute = mock(AttributeModel.class); + when(mockForeignAttribute.key()).thenReturn(filterAttributeKey); + + when(this.mockScopeStringTranslator.fromExternal(idScopeExternal)).thenReturn(idScope); + when(this.mockAttributeStore.getForeignIdAttribute(mockRequestContext, currentScope, idScope)) + .thenReturn(Single.just(mockForeignAttribute)); + + AttributeAssociation builtFilter = + this.requestBuilder + .build(this.mockRequestContext, currentScope, List.of(filterArgument)) + .blockingGet() + .get(0); + + assertEquals(mockForeignAttribute, builtFilter.attribute()); + assertEquals( + AttributeExpression.forAttributeKey(filterAttributeKey), + builtFilter.value().keyExpression()); + assertEquals(filterOperatorType, builtFilter.value().operator()); + assertEquals(filterValue, builtFilter.value().value()); + assertEquals(FilterType.ATTRIBUTE, builtFilter.value().type()); + } + + @Test + void canBuildFilterWithAttributeExpression() { + final FilterType filterType = FilterType.ATTRIBUTE; + final String filterAttributeKey = "filterKey"; + final String currentScope = "scope"; + + final FilterArgument filterArgument = mock(FilterArgument.class); + when(filterArgument.type()).thenReturn(filterType); + when(filterArgument.keyExpression()) + .thenReturn(AttributeExpression.forAttributeKey(filterAttributeKey)); + + final AttributeModel mockAttribute = mock(AttributeModel.class); + when(this.mockAttributeAssociator.associateAttribute( + same(mockRequestContext), + eq(currentScope), + any(FilterArgument.class), + eq(filterAttributeKey))) + .thenAnswer( + invocation -> + Single.just(AttributeAssociation.of(mockAttribute, invocation.getArgument(2)))); + + AttributeAssociation builtFilter = + this.requestBuilder + .build(this.mockRequestContext, currentScope, List.of(filterArgument)) + .blockingGet() + .get(0); + + assertEquals(mockAttribute, builtFilter.attribute()); + assertEquals( + AttributeExpression.forAttributeKey(filterAttributeKey), + builtFilter.value().keyExpression()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilderTest.java new file mode 100644 index 00000000..84828b34 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/request/DefaultResultSetRequestBuilderTest.java @@ -0,0 +1,120 @@ +package org.hypertrace.core.graphql.common.request; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.space.SpaceArgument; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultResultSetRequestBuilderTest { + @Mock GraphQlRequestContext mockContext; + @Mock ArgumentDeserializer mockArgumentDeserializer; + @Mock GraphQlSelectionFinder mockSelectionFinder; + @Mock AttributeRequestBuilder mockAttributeRequestBuilder; + @Mock AttributeAssociator mockAttributeAssociator; + @Mock FilterRequestBuilder mockFilterBuilder; + @Mock TimeRangeArgument mockTimeRange; + @Mock OrderArgument mockOrderArgument; + @Mock FilterArgument mockFilterArgument; + @Mock AttributeModel mockFooAttribute; + int mockLimit = 3; + int mockOffset = 4; + String mockSpace = "mock-space"; + @Mock DataFetchingFieldSelectionSet mockSelectionSet; + @Mock Stream mockAttributeQueryableStream; + @Mock AttributeRequest mockFooAttributeRequest; + @Mock AttributeRequest mockIdAttributeRequest; + + private DefaultResultSetRequestBuilder requestBuilder; + + @BeforeEach + void beforeEach() { + this.requestBuilder = + new DefaultResultSetRequestBuilder( + this.mockArgumentDeserializer, + this.mockSelectionFinder, + this.mockAttributeRequestBuilder, + this.mockAttributeAssociator, + this.mockFilterBuilder); + } + + @Test + void canBuildRequest() { + when(this.mockAttributeAssociator.associateAttributes( + any(), eq("SPAN"), eq(List.of(this.mockOrderArgument)), any())) + .thenReturn( + Observable.just( + AttributeAssociation.of(this.mockFooAttribute, this.mockOrderArgument))); + when(this.mockFilterBuilder.build(any(), eq("SPAN"), eq(List.of(this.mockFilterArgument)))) + .thenReturn( + Single.just( + List.of(AttributeAssociation.of(this.mockFooAttribute, this.mockFilterArgument)))); + when(this.mockSelectionFinder.findSelections(eq(this.mockSelectionSet), any())) + .thenReturn(mockAttributeQueryableStream); + when(this.mockAttributeRequestBuilder.buildForAttributeQueryableFieldsAndId( + any(), any(), eq(this.mockAttributeQueryableStream))) + .thenReturn(Observable.just(this.mockFooAttributeRequest, this.mockIdAttributeRequest)); + when(this.mockAttributeRequestBuilder.buildForId(any(), any())) + .thenReturn(Single.just(this.mockIdAttributeRequest)); + when(this.mockArgumentDeserializer.deserializePrimitive(any(), eq(LimitArgument.class))) + .thenReturn(Optional.of(this.mockLimit)); + when(this.mockArgumentDeserializer.deserializePrimitive(any(), eq(OffsetArgument.class))) + .thenReturn(Optional.of(this.mockOffset)); + when(this.mockArgumentDeserializer.deserializeObject(any(), eq(TimeRangeArgument.class))) + .thenReturn(Optional.of(this.mockTimeRange)); + when(this.mockArgumentDeserializer.deserializeObjectList(any(), eq(OrderArgument.class))) + .thenReturn(Optional.of(List.of(this.mockOrderArgument))); + when(this.mockArgumentDeserializer.deserializeObjectList(any(), eq(FilterArgument.class))) + .thenReturn(Optional.of(List.of(this.mockFilterArgument))); + when(this.mockArgumentDeserializer.deserializePrimitive(any(), eq(SpaceArgument.class))) + .thenReturn(Optional.of(this.mockSpace)); + + ResultSetRequest request = + this.requestBuilder + .build( + this.mockContext, + "SPAN", + Collections.emptyMap(), // Arg parser mocked, don't need values + this.mockSelectionSet) + .blockingGet(); + + assertEquals( + Set.of(this.mockIdAttributeRequest, this.mockFooAttributeRequest), request.attributes()); + assertEquals(this.mockLimit, request.limit()); + assertEquals(this.mockOffset, request.offset()); + assertEquals(this.mockTimeRange, request.timeRange()); + assertEquals( + List.of(AttributeAssociation.of(this.mockFooAttribute, this.mockOrderArgument)), + request.orderArguments()); + assertEquals( + List.of(AttributeAssociation.of(this.mockFooAttribute, this.mockFilterArgument)), + request.filterArguments()); + assertEquals(Optional.of(mockSpace), request.spaceId()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DateTimeScalarTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DateTimeScalarTest.java new file mode 100644 index 00000000..e78f5e7c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DateTimeScalarTest.java @@ -0,0 +1,85 @@ +package org.hypertrace.core.graphql.common.schema.scalars; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.language.StringValue; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.Instant; +import java.time.ZoneOffset; +import org.hypertrace.core.graphql.common.schema.typefunctions.DateTimeScalar; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DateTimeScalarTest { + + private static final String TEST_UTC_DATE_TIME_STRING = "2019-10-29T21:30:12.871Z"; + private static final String TEST_ZONED_DATE_TIME_STRING = "2019-10-30T03:00:12.871+05:30"; + private static final Instant TEST_DATE_TIME_INSTANT = Instant.parse(TEST_UTC_DATE_TIME_STRING); + private DateTimeScalar dateTimeFunction; + private GraphQLScalarType dateTimeType; + @Mock AnnotatedType mockAnnotatedType; + // Can't actually mock class, but it's not used so to convey intent using the Mock class. + private final Class mockAnnotatedClass = Mock.class; + @Mock ProcessingElementsContainer mockProcessingElementsContainer; + + @BeforeEach + void beforeEach() { + this.dateTimeFunction = new DateTimeScalar(); + // Can't actually mock class, but it's not used so to convey intent using + this.dateTimeType = + this.dateTimeFunction.buildType( + false, mockAnnotatedClass, mockAnnotatedType, mockProcessingElementsContainer); + } + + @Test + void canDetermineIfConvertible() { + assertTrue(this.dateTimeFunction.canBuildType(Instant.class, this.mockAnnotatedType)); + } + + @Test + void canConvertFromLiteral() { + assertEquals( + TEST_DATE_TIME_INSTANT, dateTimeType.getCoercing().parseLiteral(TEST_UTC_DATE_TIME_STRING)); + assertEquals( + TEST_DATE_TIME_INSTANT, + dateTimeType.getCoercing().parseLiteral(TEST_ZONED_DATE_TIME_STRING)); + } + + @Test + void canSerialize() { + assertEquals( + TEST_UTC_DATE_TIME_STRING, dateTimeType.getCoercing().serialize(TEST_DATE_TIME_INSTANT)); + assertEquals( + TEST_UTC_DATE_TIME_STRING, dateTimeType.getCoercing().serialize(TEST_UTC_DATE_TIME_STRING)); + assertEquals( + TEST_UTC_DATE_TIME_STRING, + dateTimeType.getCoercing().serialize(TEST_ZONED_DATE_TIME_STRING)); + + assertEquals( + TEST_UTC_DATE_TIME_STRING, + dateTimeType + .getCoercing() + .serialize(TEST_DATE_TIME_INSTANT.atOffset(ZoneOffset.ofHoursMinutes(12, 30)))); + } + + @Test + void canConvertFromValue() { + assertEquals( + TEST_DATE_TIME_INSTANT, + dateTimeType + .getCoercing() + .parseValue(StringValue.newStringValue().value(TEST_UTC_DATE_TIME_STRING).build())); + assertEquals( + TEST_DATE_TIME_INSTANT, + dateTimeType + .getCoercing() + .parseValue(StringValue.newStringValue().value(TEST_ZONED_DATE_TIME_STRING).build())); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DurationScalarTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DurationScalarTest.java new file mode 100644 index 00000000..f5165290 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/DurationScalarTest.java @@ -0,0 +1,66 @@ +package org.hypertrace.core.graphql.common.schema.scalars; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.language.StringValue; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.Duration; +import java.time.Instant; +import org.hypertrace.core.graphql.common.schema.typefunctions.DurationScalar; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DurationScalarTest { + + private static final String TEST_DURATION_STRING = "PT2H30M"; + private static final Duration TEST_DURATION = Duration.parse(TEST_DURATION_STRING); + private DurationScalar durationScalar; + private GraphQLScalarType durationType; + @Mock AnnotatedType mockAnnotatedType; + // Can't actually mock class, but it's not used so to convey intent using the Mock class. + private final Class mockAnnotatedClass = Mock.class; + @Mock ProcessingElementsContainer mockProcessingElementsContainer; + + @BeforeEach + void beforeEach() { + this.durationScalar = new DurationScalar(); + // Can't actually mock class, but it's not used so to convey intent using + this.durationType = + this.durationScalar.buildType( + false, mockAnnotatedClass, mockAnnotatedType, mockProcessingElementsContainer); + } + + @Test + void canDetermineIfConvertible() { + assertTrue(this.durationScalar.canBuildType(Duration.class, this.mockAnnotatedType)); + assertFalse(this.durationScalar.canBuildType(Instant.class, this.mockAnnotatedType)); + } + + @Test + void canConvertFromLiteral() { + assertEquals(TEST_DURATION, durationType.getCoercing().parseLiteral(TEST_DURATION_STRING)); + } + + @Test + void canSerialize() { + assertEquals(TEST_DURATION_STRING, durationType.getCoercing().serialize(TEST_DURATION)); + assertEquals(TEST_DURATION_STRING, durationType.getCoercing().serialize(TEST_DURATION_STRING)); + } + + @Test + void canConvertFromValue() { + assertEquals( + TEST_DURATION, + durationType + .getCoercing() + .parseValue(StringValue.newStringValue().value(TEST_DURATION_STRING).build())); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/OffsetTimeScalarTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/OffsetTimeScalarTest.java new file mode 100644 index 00000000..ed8382ae --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/OffsetTimeScalarTest.java @@ -0,0 +1,72 @@ +package org.hypertrace.core.graphql.common.schema.scalars; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.language.StringValue; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import org.hypertrace.core.graphql.common.schema.typefunctions.OffsetTimeScalar; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OffsetTimeScalarTest { + + private static final String TEST_SCALAR_TIME_STRING = "21:30:12.748+12:30"; + private static final OffsetTime TEST_OFFSET_TIME = OffsetTime.parse(TEST_SCALAR_TIME_STRING); + private OffsetTimeScalar offsetTimeFunction; + private GraphQLScalarType offsetTimeType; + @Mock AnnotatedType mockAnnotatedType; + // Can't actually mock class, but it's not used so to convey intent using the Mock class. + private final Class mockAnnotatedClass = Mock.class; + @Mock ProcessingElementsContainer mockProcessingElementsContainer; + + @BeforeEach + void beforeEach() { + this.offsetTimeFunction = new OffsetTimeScalar(); + // Can't actually mock class, but it's not used so to convey intent using + this.offsetTimeType = + this.offsetTimeFunction.buildType( + false, mockAnnotatedClass, mockAnnotatedType, mockProcessingElementsContainer); + } + + @Test + void canDetermineIfConvertible() { + assertTrue(this.offsetTimeFunction.canBuildType(OffsetTime.class, this.mockAnnotatedType)); + } + + @Test + void canConvertFromLiteral() { + assertEquals( + TEST_OFFSET_TIME, offsetTimeType.getCoercing().parseLiteral(TEST_SCALAR_TIME_STRING)); + } + + @Test + void canSerialize() { + assertEquals(TEST_SCALAR_TIME_STRING, offsetTimeType.getCoercing().serialize(TEST_OFFSET_TIME)); + assertEquals( + TEST_SCALAR_TIME_STRING, offsetTimeType.getCoercing().serialize(TEST_SCALAR_TIME_STRING)); + + assertEquals( + TEST_SCALAR_TIME_STRING, + offsetTimeType + .getCoercing() + .serialize(TEST_OFFSET_TIME.withOffsetSameLocal(ZoneOffset.ofHoursMinutes(12, 30)))); + } + + @Test + void canConvertFromValue() { + assertEquals( + TEST_OFFSET_TIME, + offsetTimeType + .getCoercing() + .parseValue(StringValue.newStringValue().value(TEST_SCALAR_TIME_STRING).build())); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/UnknownScalarTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/UnknownScalarTest.java new file mode 100644 index 00000000..e3f33be3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/schema/scalars/UnknownScalarTest.java @@ -0,0 +1,114 @@ +package org.hypertrace.core.graphql.common.schema.scalars; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.language.ArrayValue; +import graphql.language.BooleanValue; +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.AnnotatedType; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.typefunctions.UnknownScalar; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UnknownScalarTest { + + private UnknownScalar unknownScalarFunction; + private GraphQLScalarType unknownScalarType; + @Mock AnnotatedType mockAnnotatedType; + // Can't actually mock class, but it's not used so to convey intent using the Mock class. + private final Class mockAnnotatedClass = Mock.class; + @Mock ProcessingElementsContainer mockProcessingElementsContainer; + + @BeforeEach + void beforeEach() { + this.unknownScalarFunction = new UnknownScalar(); + // Can't actually mock class, but it's not used so to convey intent using + this.unknownScalarType = + this.unknownScalarFunction.buildType( + false, mockAnnotatedClass, mockAnnotatedType, mockProcessingElementsContainer); + } + + @Test + void canDetermineIfConvertible() { + // We should only use unknown if we can't type narrower than object in java + assertTrue(this.unknownScalarFunction.canBuildType(Object.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(String.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Integer.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Long.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Instant.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Float.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Double.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Boolean.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(List.class, this.mockAnnotatedType)); + assertFalse(this.unknownScalarFunction.canBuildType(Enum.class, this.mockAnnotatedType)); + } + + @Test + void canSerialize() { + assertEquals("five", unknownScalarType.getCoercing().serialize("five")); + assertEquals(5, unknownScalarType.getCoercing().serialize(5)); + assertEquals(5.0d, unknownScalarType.getCoercing().serialize(5.0d)); + assertEquals(true, unknownScalarType.getCoercing().serialize(true)); + assertEquals(List.of(5), unknownScalarType.getCoercing().serialize(List.of(5))); + } + + @Test + void canConvertFromLiteral() { + assertEquals( + "five", + unknownScalarType.getCoercing().parseLiteral(StringValue.newStringValue("five").build())); + assertEquals( + BigInteger.valueOf(5), + unknownScalarType + .getCoercing() + .parseLiteral(IntValue.newIntValue(BigInteger.valueOf(5)).build())); + assertEquals( + BigDecimal.valueOf(5.0d), + unknownScalarType + .getCoercing() + .parseLiteral(FloatValue.newFloatValue(BigDecimal.valueOf(5.0d)).build())); + assertEquals( + true, + unknownScalarType.getCoercing().parseLiteral(BooleanValue.newBooleanValue(true).build())); + assertEquals( + List.of("five"), + unknownScalarType + .getCoercing() + .parseLiteral( + ArrayValue.newArrayValue() + .value(StringValue.newStringValue("five").build()) + .build())); + + assertThrows( + CoercingParseLiteralException.class, + () -> unknownScalarType.getCoercing().parseLiteral("bad value")); + } + + @Test + void canConvertFromValue() { + // A dumb bug requires a dumb test + assertEquals(true, unknownScalarType.getCoercing().parseValue(true)); + + assertEquals("value", unknownScalarType.getCoercing().parseValue("value")); + + assertEquals(10, unknownScalarType.getCoercing().parseValue(10)); + + assertEquals(10.5, unknownScalarType.getCoercing().parseValue(10.5)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverterTest.java new file mode 100644 index 00000000..4e9383db --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-common-schema/src/test/java/org/hypertrace/core/graphql/common/utils/attributes/AttributeTypeConverterTest.java @@ -0,0 +1,44 @@ +package org.hypertrace.core.graphql.common.utils.attributes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.junit.jupiter.api.Test; + +public class AttributeTypeConverterTest { + @Test + void testConvert() { + AttributeTypeConverter attributeTypeConverter = new AttributeTypeConverter(); + + assertEquals( + AttributeType.STRING, + attributeTypeConverter.convert(AttributeModelType.STRING).blockingGet()); + assertEquals( + AttributeType.BOOLEAN, + attributeTypeConverter.convert(AttributeModelType.BOOLEAN).blockingGet()); + assertEquals( + AttributeType.LONG, attributeTypeConverter.convert(AttributeModelType.LONG).blockingGet()); + assertEquals( + AttributeType.DOUBLE, + attributeTypeConverter.convert(AttributeModelType.DOUBLE).blockingGet()); + assertEquals( + AttributeType.TIMESTAMP, + attributeTypeConverter.convert(AttributeModelType.TIMESTAMP).blockingGet()); + assertEquals( + AttributeType.STRING_MAP, + attributeTypeConverter.convert(AttributeModelType.STRING_MAP).blockingGet()); + assertEquals( + AttributeType.STRING_ARRAY, + attributeTypeConverter.convert(AttributeModelType.STRING_ARRAY).blockingGet()); + assertEquals( + AttributeType.DOUBLE_ARRAY, + attributeTypeConverter.convert(AttributeModelType.DOUBLE_ARRAY).blockingGet()); + assertEquals( + AttributeType.LONG_ARRAY, + attributeTypeConverter.convert(AttributeModelType.LONG_ARRAY).blockingGet()); + assertEquals( + AttributeType.BOOLEAN_ARRAY, + attributeTypeConverter.convert(AttributeModelType.BOOLEAN_ARRAY).blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-context/build.gradle.kts new file mode 100644 index 00000000..02ee6d2c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(localLibs.graphql.servlet) + + implementation(projects.hypertraceCoreGraphqlSpi) + implementation(commonLibs.guava) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-context/gradle.lockfile new file mode 100644 index 00000000..8067edf1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/gradle.lockfile @@ -0,0 +1,55 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactory.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactory.java new file mode 100644 index 00000000..f72225fd --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactory.java @@ -0,0 +1,63 @@ +package org.hypertrace.core.graphql.context; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.inject.Injector; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.AllArgsConstructor; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; + +@Singleton +class AsyncDataFetcherFactory { + + private final Injector injector; + private final GraphQlEndpointConfig endpointConfig; + private final ExecutorService requestExecutor; + + @Inject + AsyncDataFetcherFactory(Injector injector, GraphQlEndpointConfig endpointConfig) { + this.injector = injector; + this.endpointConfig = endpointConfig; + this.requestExecutor = + Executors.newFixedThreadPool( + endpointConfig.getMaxRequestThreads(), + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("request-handler-%d").build()); + } + + DataFetcher> buildDataFetcher( + Class>> dataFetcherClass) { + return new AsyncForwardingDataFetcher<>( + this.injector.getInstance(dataFetcherClass), requestExecutor, endpointConfig); + } + + @AllArgsConstructor + private static class AsyncForwardingDataFetcher implements DataFetcher> { + private final DataFetcher> delegate; + private final ExecutorService executorService; + private final GraphQlEndpointConfig config; + + @Override + public CompletableFuture get(DataFetchingEnvironment dataFetchingEnvironment) + throws Exception { + // Really all we're doing here is changing the thread that the future is run on by default + return CompletableFuture.supplyAsync( + () -> { + try { + return delegate + .get(dataFetchingEnvironment) + .get(config.getTimeout().toMillis(), MILLISECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, + executorService); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/ContextualCachingKey.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/ContextualCachingKey.java new file mode 100644 index 00000000..be32b654 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/ContextualCachingKey.java @@ -0,0 +1,5 @@ +package org.hypertrace.core.graphql.context; + +public interface ContextualCachingKey { + GraphQlRequestContext getContext(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilder.java new file mode 100644 index 00000000..91f9fdc1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilder.java @@ -0,0 +1,131 @@ +package org.hypertrace.core.graphql.context; + +import com.google.common.collect.Streams; +import graphql.kickstart.execution.context.DefaultGraphQLContext; +import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; +import graphql.schema.DataFetcher; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; + +class DefaultGraphQlRequestContextBuilder extends DefaultGraphQLServletContextBuilder + implements GraphQlRequestContextBuilder { + private static final String DEFAULT_CONTEXT_ID = "DEFAULT_CONTEXT_ID"; + static final String AUTHORIZATION_HEADER_KEY = "Authorization"; + static final String TENANT_ID_HEADER_KEY = "x-tenant-id"; + static final Set TRACING_CONTEXT_HEADER_KEY_PREFIXES = + Set.of("X-B3-", "traceparent", "tracestate"); + + private final GraphQlServiceConfig serviceConfig; + private final AsyncDataFetcherFactory dataFetcherFactory; + + @Inject + DefaultGraphQlRequestContextBuilder( + AsyncDataFetcherFactory dataFetcherFactory, GraphQlServiceConfig serviceConfig) { + this.dataFetcherFactory = dataFetcherFactory; + this.serviceConfig = serviceConfig; + } + + @Override + public GraphQlRequestContext build(HttpServletRequest request, HttpServletResponse response) { + return new DefaultGraphQlRequestContext(request); + } + + private final class DefaultGraphQlRequestContext extends DefaultGraphQLContext + implements GraphQlRequestContext { + private final ContextualCachingKey cachingKey; + + private final String requestId = UUID.randomUUID().toString(); + private final HttpServletRequest request; + + private DefaultGraphQlRequestContext(HttpServletRequest request) { + this.request = request; + this.cachingKey = + new DefaultContextualCacheKey(this, this.getTenantId().orElse(DEFAULT_CONTEXT_ID)); + this.put(GraphQlRequestContext.class, this); + } + + @Override + public DataFetcher> constructDataFetcher( + Class>> dataFetcherClass) { + return DefaultGraphQlRequestContextBuilder.this.dataFetcherFactory.buildDataFetcher( + dataFetcherClass); + } + + @Override + public Optional getAuthorizationHeader() { + return Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER_KEY)) + .or(() -> Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER_KEY.toLowerCase()))); + } + + @Override + public Optional getTenantId() { + return Optional.ofNullable(request.getHeader(TENANT_ID_HEADER_KEY)) + .or(DefaultGraphQlRequestContextBuilder.this.serviceConfig::getDefaultTenantId); + } + + @Override + public Map getTracingContextHeaders() { + return Streams.stream(request.getHeaderNames().asIterator()) + .filter( + header -> + TRACING_CONTEXT_HEADER_KEY_PREFIXES.stream() + .anyMatch(prefix -> header.toLowerCase().startsWith(prefix.toLowerCase()))) + .collect(Collectors.toUnmodifiableMap(String::toLowerCase, request::getHeader)); + } + + @Nonnull + @Override + public ContextualCachingKey getCachingKey() { + return this.cachingKey; + } + + @Nonnull + @Override + public String getRequestId() { + return this.requestId; + } + } + + private static class DefaultContextualCacheKey implements ContextualCachingKey { + + private final GraphQlRequestContext context; + private final Object[] cacheInputs; + + private DefaultContextualCacheKey(GraphQlRequestContext context, Object... cacheInputs) { + this.context = context; + this.cacheInputs = cacheInputs; + } + + @Override + public GraphQlRequestContext getContext() { + return this.context; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultContextualCacheKey that = (DefaultContextualCacheKey) o; + return Arrays.equals(cacheInputs, that.cacheInputs); + } + + @Override + public int hashCode() { + return Arrays.hashCode(cacheInputs); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContext.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContext.java new file mode 100644 index 00000000..d943ea0f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContext.java @@ -0,0 +1,34 @@ +package org.hypertrace.core.graphql.context; + +import graphql.kickstart.execution.context.GraphQLKickstartContext; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +public interface GraphQlRequestContext extends GraphQLKickstartContext { + + /** + * A tool to create data fetchers via injection container due to limitations in the framework. For + * normal injectable instantiation, do not use this method. + */ + DataFetcher> constructDataFetcher( + Class>> dataFetcherClass); + + Optional getAuthorizationHeader(); + + Optional getTenantId(); + + Map getTracingContextHeaders(); + + @Nonnull + ContextualCachingKey getCachingKey(); + + String getRequestId(); + + static GraphQlRequestContext contextFromEnvironment(DataFetchingEnvironment environment) { + return environment.getGraphQlContext().get(GraphQlRequestContext.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextBuilder.java new file mode 100644 index 00000000..6d59a530 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextBuilder.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.context; + +import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface GraphQlRequestContextBuilder extends GraphQLServletContextBuilder { + @Override + GraphQlRequestContext build( + HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextModule.java new file mode 100644 index 00000000..de1ce1d2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/main/java/org/hypertrace/core/graphql/context/GraphQlRequestContextModule.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.context; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; + +public class GraphQlRequestContextModule extends AbstractModule { + @Override + protected void configure() { + bind(GraphQlRequestContextBuilder.class).to(DefaultGraphQlRequestContextBuilder.class); + requireBinding(GraphQlServiceConfig.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactoryTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactoryTest.java new file mode 100644 index 00000000..62f12e1a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/AsyncDataFetcherFactoryTest.java @@ -0,0 +1,40 @@ +package org.hypertrace.core.graphql.context; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import com.google.inject.Guice; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AsyncDataFetcherFactoryTest { + @Mock GraphQlEndpointConfig endpointConfig; + @Mock DataFetchingEnvironment dataFetchingEnvironment; + + @Test + void canBuildAsyncDataFetcher() throws Exception { + when(endpointConfig.getMaxRequestThreads()).thenReturn(1); + DataFetcher> fetcher = + new AsyncDataFetcherFactory(Guice.createInjector(), endpointConfig) + .buildDataFetcher(ThreadEchoingDataFetcher.class); + + Thread fetcherThread = fetcher.get(dataFetchingEnvironment).get(); + + assertNotEquals(Thread.currentThread(), fetcherThread); + assertNotNull(fetcherThread); + } + + private static class ThreadEchoingDataFetcher implements DataFetcher> { + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + return CompletableFuture.completedFuture(Thread.currentThread()); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilderTest.java new file mode 100644 index 00000000..c3c82e5a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-context/src/test/java/org/hypertrace/core/graphql/context/DefaultGraphQlRequestContextBuilderTest.java @@ -0,0 +1,140 @@ +package org.hypertrace.core.graphql.context; + +import static java.util.Collections.emptyMap; +import static org.hypertrace.core.graphql.context.DefaultGraphQlRequestContextBuilder.AUTHORIZATION_HEADER_KEY; +import static org.hypertrace.core.graphql.context.DefaultGraphQlRequestContextBuilder.TENANT_ID_HEADER_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetcher; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultGraphQlRequestContextBuilderTest { + + @Mock AsyncDataFetcherFactory mockDataFetcherFactory; + @Mock HttpServletRequest mockRequest; + @Mock HttpServletResponse mockResponse; + @Mock GraphQlServiceConfig mockServiceConfig; + + GraphQlRequestContextBuilder contextBuilder; + GraphQlRequestContext requestContext; + + @BeforeEach + void beforeEach() { + this.contextBuilder = + new DefaultGraphQlRequestContextBuilder( + this.mockDataFetcherFactory, this.mockServiceConfig); + this.requestContext = this.contextBuilder.build(this.mockRequest, this.mockResponse); + } + + @Test + void returnsAuthorizationHeaderIfPresent() { + when(this.mockRequest.getHeader(eq(AUTHORIZATION_HEADER_KEY))).thenReturn("Bearer ABC"); + when(this.mockRequest.getHeader(eq(AUTHORIZATION_HEADER_KEY.toLowerCase()))) + .thenReturn("Bearer abc"); + assertEquals(Optional.of("Bearer ABC"), this.requestContext.getAuthorizationHeader()); + + when(this.mockRequest.getHeader(eq(AUTHORIZATION_HEADER_KEY))).thenReturn(null); + assertEquals(Optional.of("Bearer abc"), this.requestContext.getAuthorizationHeader()); + } + + @Test + void returnsEmptyOptionalIfNoAuthorizationHeaderPresent() { + when(this.mockRequest.getHeader(any())).thenReturn(null); + assertEquals(Optional.empty(), this.requestContext.getAuthorizationHeader()); + } + + @Test + void delegatesDataLoaderRegistry() { + assertNotNull(this.requestContext.getDataLoaderRegistry()); + } + + @Test + void canDelegateDataFetcherConstruction() { + this.requestContext.constructDataFetcher(TestDataFetcher.class); + verify(this.mockDataFetcherFactory).buildDataFetcher(TestDataFetcher.class); + } + + @Test + void returnsTenantIdIfTenantIdHeaderPresent() { + when(this.mockRequest.getHeader(TENANT_ID_HEADER_KEY)).thenReturn("test tenant id"); + assertEquals(Optional.of("test tenant id"), this.requestContext.getTenantId()); + } + + @Test + void returnsDefaultTenantIdOnlyIfNoHeaderPresent() { + when(this.mockRequest.getHeader(TENANT_ID_HEADER_KEY)).thenReturn("test tenant id"); + when(this.mockServiceConfig.getDefaultTenantId()).thenReturn(Optional.of("default tenant id")); + assertEquals(Optional.of("test tenant id"), this.requestContext.getTenantId()); + reset(this.mockRequest); + assertEquals(Optional.of("default tenant id"), this.requestContext.getTenantId()); + } + + @Test + void returnsCachingKeyForNoAuth() { + assertNotNull(this.requestContext.getCachingKey()); + } + + @Test + void returnsCachingKeysEqualForSameTenant() { + when(this.mockRequest.getHeader(TENANT_ID_HEADER_KEY)).thenReturn("first tenant id"); + var firstKey = this.contextBuilder.build(this.mockRequest, this.mockResponse).getCachingKey(); + var secondKey = this.contextBuilder.build(this.mockRequest, this.mockResponse).getCachingKey(); + assertEquals(firstKey, secondKey); + assertNotSame(firstKey, secondKey); + + when(this.mockRequest.getHeader(TENANT_ID_HEADER_KEY)).thenReturn("second tenant id"); + var thirdKey = this.contextBuilder.build(this.mockRequest, this.mockResponse).getCachingKey(); + assertNotEquals(firstKey, thirdKey); + } + + @Test + void returnsEmptyMapIfNoTracingHeadersPresent() { + when(this.mockRequest.getHeaderNames()).thenReturn(Collections.enumeration(List.of("foo"))); + assertEquals(emptyMap(), this.requestContext.getTracingContextHeaders()); + } + + @Test + void returnsLowerCasedTracingHeadersIfAnyMatches() { + when(this.mockRequest.getHeaderNames()) + .thenReturn( + Collections.enumeration( + List.of( + "traceSTATE", "traceparent", "other", "X-B3-traceid", "x-b3-parent-trace-id"))); + when(this.mockRequest.getHeader(any(String.class))) + .thenAnswer(invocation -> invocation.getArgument(0) + " value"); + assertEquals( + Map.of( + "tracestate", + "traceSTATE value", + "traceparent", + "traceparent value", + "x-b3-traceid", + "X-B3-traceid value", + "x-b3-parent-trace-id", + "x-b3-parent-trace-id value"), + this.requestContext.getTracingContextHeaders()); + } + + private interface TestDataFetcher extends DataFetcher> {} +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/build.gradle.kts new file mode 100644 index 00000000..d2164986 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.jackson.databind) + api(commonLibs.graphql.java) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + implementation(commonLibs.jackson.datatype.jsr310) + implementation(commonLibs.jackson.datatype.jdk8) + implementation(commonLibs.slf4j2.api) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/gradle.lockfile new file mode 100644 index 00000000..7553779b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/gradle.lockfile @@ -0,0 +1,49 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializationConfig.java new file mode 100644 index 00000000..8a62aaae --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializationConfig.java @@ -0,0 +1,29 @@ +package org.hypertrace.core.graphql.deserialization; + +import com.fasterxml.jackson.databind.Module; +import java.util.Collections; +import java.util.List; + +public interface ArgumentDeserializationConfig { + + String getArgumentKey(); + + default String getListArgumentKey() { + return getArgumentKey(); + } + + Class getArgumentSchema(); + + default List jacksonModules() { + return Collections.emptyList(); + } + + static ArgumentDeserializationConfig forPrimitive(String argKey, Class argSchema) { + return forPrimitive(argKey, argKey, argSchema); + } + + static ArgumentDeserializationConfig forPrimitive( + String argKey, String listArgKey, Class argSchema) { + return new SimpleArgumentDeserializationConfig(argKey, listArgKey, argSchema); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializer.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializer.java new file mode 100644 index 00000000..1b464b8d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/ArgumentDeserializer.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.deserialization; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface ArgumentDeserializer { + + Optional deserializeObject(Map arguments, Class argSchema); + + Optional> deserializeObjectList(Map arguments, Class argSchema); + + Optional deserializePrimitive( + Map arguments, Class> argSchema); + + Optional> deserializePrimitiveList( + Map arguments, Class> argSchema); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializer.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializer.java new file mode 100644 index 00000000..4377a76c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializer.java @@ -0,0 +1,133 @@ +package org.hypertrace.core.graphql.deserialization; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class DefaultArgumentDeserializer implements ArgumentDeserializer { + + private static final List defaultModules = + List.of(new JavaTimeModule(), new Jdk8Module()); + private static final Logger LOG = LoggerFactory.getLogger(DefaultArgumentDeserializer.class); + + private final ObjectMapper objectMapper; + private final Map, ArgumentDeserializationConfig> configBySchema; + + @Inject + DefaultArgumentDeserializer(Set argumentDeserializationConfigs) { + Set jacksonModules = + Stream.concat( + defaultModules.stream(), + argumentDeserializationConfigs.stream() + .map(ArgumentDeserializationConfig::jacksonModules) + .flatMap(Collection::stream)) + .collect(Collectors.toUnmodifiableSet()); + + this.configBySchema = + argumentDeserializationConfigs.stream() + .collect( + Collectors.toUnmodifiableMap( + ArgumentDeserializationConfig::getArgumentSchema, Function.identity())); + + this.objectMapper = JsonMapper.builder().addModules(jacksonModules).build(); + } + + @Override + public Optional deserializeObject(Map arguments, Class argSchema) { + return this.argumentKey(argSchema) + .flatMap(key -> this.deserializeValue(arguments.get(key), argSchema)); + } + + @Override + public Optional> deserializeObjectList( + Map arguments, Class argSchema) { + return this.listArgumentKey(argSchema) + .flatMap(key -> this.deserializeValueList(arguments.get(key), argSchema)); + } + + @Override + public Optional deserializePrimitive( + Map arguments, Class> argSchema) { + return this.argumentKey(argSchema).map(key -> this.uncheckedCast(arguments.get(key))); + } + + @Override + public Optional> deserializePrimitiveList( + Map arguments, Class> argSchema) { + return this.listArgumentKey(argSchema).flatMap(key -> this.logIfNotList(arguments.get(key))); + } + + private Optional deserializeValue(@Nullable Object rawValue, Class argSchema) { + try { + return Optional.ofNullable(rawValue) + .map(presentRawValue -> this.objectMapper.convertValue(presentRawValue, argSchema)); + } catch (Throwable t) { + LOG.warn("Failed to deserialize for argument type '{}'", argSchema, t); + return Optional.empty(); + } + } + + private Optional argumentKey(Class argSchema) { + return configForSchema(argSchema).map(ArgumentDeserializationConfig::getArgumentKey); + } + + private Optional listArgumentKey(Class argSchema) { + return configForSchema(argSchema).map(ArgumentDeserializationConfig::getListArgumentKey); + } + + private Optional configForSchema(Class argSchema) { + return Optional.ofNullable(this.configBySchema.get(argSchema)) + .or( + () -> { + LOG.warn( + "No deserialization config registered for provided class '{}'", + argSchema.getCanonicalName()); + + return Optional.empty(); + }); + } + + private Optional> deserializeValueList( + @Nullable Object rawValueList, Class argSchema) { + try { + return this.logIfNotList(rawValueList) + .map( + list -> + list.stream() + .map(rawValue -> this.deserializeValue(rawValue, argSchema).orElseThrow()) + .collect(Collectors.toUnmodifiableList())); + } catch (Exception e) { + return Optional.empty(); + } + } + + private Optional> logIfNotList(@Nullable Object object) { + if (object instanceof List) { + return Optional.of(this.uncheckedCast(object)); + } + if (Objects.nonNull(object)) { + LOG.warn("Expected to deserialize list but instead got {}", object); + } + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + private T uncheckedCast(Object value) { + return (T) value; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/GraphQlDeserializationRegistryModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/GraphQlDeserializationRegistryModule.java new file mode 100644 index 00000000..cabd8d88 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/GraphQlDeserializationRegistryModule.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.deserialization; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; + +public class GraphQlDeserializationRegistryModule extends AbstractModule { + + @Override + protected void configure() { + bind(ArgumentDeserializer.class).to(DefaultArgumentDeserializer.class); + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/PrimitiveArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/PrimitiveArgument.java new file mode 100644 index 00000000..bac71cd5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/PrimitiveArgument.java @@ -0,0 +1,3 @@ +package org.hypertrace.core.graphql.deserialization; + +public interface PrimitiveArgument {} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/SimpleArgumentDeserializationConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/SimpleArgumentDeserializationConfig.java new file mode 100644 index 00000000..06cd34a7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/main/java/org/hypertrace/core/graphql/deserialization/SimpleArgumentDeserializationConfig.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.deserialization; + +import lombok.Value; + +@Value +class SimpleArgumentDeserializationConfig implements ArgumentDeserializationConfig { + String argumentKey; + String listArgumentKey; + Class argumentSchema; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/test/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializerTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/test/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializerTest.java new file mode 100644 index 00000000..de92c858 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-deserialization/src/test/java/org/hypertrace/core/graphql/deserialization/DefaultArgumentDeserializerTest.java @@ -0,0 +1,202 @@ +package org.hypertrace.core.graphql.deserialization; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultArgumentDeserializerTest { + + private DefaultArgumentDeserializer argumentDeserializer; + + private interface TestPrimitiveArgument extends PrimitiveArgument { + String ARG_NAME = "primitiveArg"; + String LIST_ARG_NAME = "primitiveArgs"; + } + + private interface TestObjectArgument { + String ARG_NAME = "objectArg"; + String VALUE_NAME = "value"; + + String value(); + } + + private static class DefaultTestObjectArgument implements TestObjectArgument { + @JsonProperty(VALUE_NAME) + private String value; + + @Override + public String value() { + return this.value; + } + } + + @BeforeEach + void beforeEach() { + this.argumentDeserializer = + new DefaultArgumentDeserializer( + Set.of( + ArgumentDeserializationConfig.forPrimitive( + TestPrimitiveArgument.ARG_NAME, + TestPrimitiveArgument.LIST_ARG_NAME, + TestPrimitiveArgument.class), + new ArgumentDeserializationConfig() { + @Override + public String getArgumentKey() { + return TestObjectArgument.ARG_NAME; + } + + @Override + public Class getArgumentSchema() { + return TestObjectArgument.class; + } + + @Override + public List jacksonModules() { + return List.of( + new SimpleModule() + .addAbstractTypeMapping( + TestObjectArgument.class, DefaultTestObjectArgument.class)); + } + })); + } + + @Test + void deserializesObjectListIfPresent() { + Map argMap = + Map.of( + TestObjectArgument.ARG_NAME, + List.of( + Map.of(TestObjectArgument.VALUE_NAME, "foo"), + Map.of(TestObjectArgument.VALUE_NAME, "bar"))); + + List result = + this.argumentDeserializer + .deserializeObjectList(argMap, TestObjectArgument.class) + .orElseThrow(); + assertEquals(2, result.size()); + assertEquals("foo", result.get(0).value()); + assertEquals("bar", result.get(1).value()); + } + + @Test + void emptyObjectListIfNotPresent() { + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializeObjectList( + Collections.emptyMap(), TestObjectArgument.class)); + } + + @Test + void emptyObjectListIfUnableToDeserialize() { + Map argMap = + Map.of(TestObjectArgument.ARG_NAME, List.of(Map.of("garbage", "more garbage"))); + + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializeObjectList(argMap, TestObjectArgument.class)); + } + + @Test + void emptyListIfObjectListPresentButEmpty() { + Map argMap = Map.of(TestObjectArgument.ARG_NAME, List.of()); + assertEquals( + Optional.of(List.of()), + this.argumentDeserializer.deserializeObjectList(argMap, TestObjectArgument.class)); + } + + @Test + void deserializeObjectIfPresent() { + Map argMap = + Map.of(TestObjectArgument.ARG_NAME, Map.of(TestObjectArgument.VALUE_NAME, "baz")); + + assertEquals( + "baz", + this.argumentDeserializer + .deserializeObject(argMap, TestObjectArgument.class) + .orElseThrow() + .value()); + } + + @Test + void emptyIfNoObjectPresent() { + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializeObject( + Collections.emptyMap(), TestObjectArgument.class)); + } + + @Test + void emptyIfDeserializationFailurePresent() { + Map argMap = + Map.of(TestObjectArgument.ARG_NAME, Map.of("garbage", "more garbage")); + + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializeObject(argMap, TestObjectArgument.class)); + } + + @Test + void deserializePrimitive() { + Map argMap = Map.of(TestPrimitiveArgument.ARG_NAME, "baz"); + + assertEquals( + Optional.of("baz"), + this.argumentDeserializer.deserializePrimitive(argMap, TestPrimitiveArgument.class)); + } + + @Test + void emptyIfNoPrimitivePresent() { + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializePrimitive( + Collections.emptyMap(), TestPrimitiveArgument.class)); + } + + @Test + void deserializePrimitiveList() { + Map argMap = + Map.of(TestPrimitiveArgument.LIST_ARG_NAME, List.of("foo", "bar", "baz")); + assertEquals( + Optional.of(List.of("foo", "bar", "baz")), + this.argumentDeserializer.deserializePrimitiveList(argMap, TestPrimitiveArgument.class)); + + // Make sure only the list arg name works + argMap = Map.of(TestPrimitiveArgument.ARG_NAME, List.of("foo", "bar", "baz")); + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializePrimitiveList(argMap, TestPrimitiveArgument.class)); + } + + @Test + void emptyListForEmptyPrimitiveList() { + Map argMap = Map.of(TestPrimitiveArgument.LIST_ARG_NAME, List.of()); + assertEquals( + Optional.of(List.of()), + this.argumentDeserializer.deserializePrimitiveList(argMap, TestPrimitiveArgument.class)); + } + + @Test + void emptyIfNoPrimitiveList() { + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializePrimitiveList( + Collections.emptyMap(), TestPrimitiveArgument.class)); + } + + @Test + void emptyIfNotList() { + Map argMap = Map.of(TestPrimitiveArgument.ARG_NAME, "baz"); + assertEquals( + Optional.empty(), + this.argumentDeserializer.deserializePrimitiveList(argMap, TestPrimitiveArgument.class)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/build.gradle.kts new file mode 100644 index 00000000..40016ea4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.hypertrace.gatewayservice.api) + api(projects.hypertraceCoreGraphqlAttributeStore) + api(commonLibs.rxjava3) + api(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/gradle.lockfile new file mode 100644 index 00000000..0145e3bb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/gradle.lockfile @@ -0,0 +1,80 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverter.java new file mode 100644 index 00000000..2f26f46e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverter.java @@ -0,0 +1,22 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.AttributeExpression.Builder; +import org.hypertrace.gateway.service.v1.common.Expression; + +class AttributeExpressionConverter + implements Converter, Expression> { + + @Override + public Single convert( + AttributeAssociation attributeExpressionAssociation) { + Builder builder = org.hypertrace.gateway.service.v1.common.AttributeExpression.newBuilder(); + builder.setAttributeId(attributeExpressionAssociation.attribute().id()); + attributeExpressionAssociation.value().subpath().ifPresent(builder::setSubpath); + builder.setAlias(attributeExpressionAssociation.value().asAlias()); + return Single.just(Expression.newBuilder().setAttributeExpression(builder).build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeMapConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeMapConverter.java new file mode 100644 index 00000000..12167d68 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/AttributeMapConverter.java @@ -0,0 +1,50 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import javax.inject.Inject; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.CollectorUtils; +import org.hypertrace.gateway.service.v1.common.Value; + +class AttributeMapConverter + implements BiConverter< + Collection, Map, Map> { + + private final BiConverter valueConverter; + + @Inject + AttributeMapConverter(BiConverter valueConverter) { + this.valueConverter = valueConverter; + } + + @Override + public Single> convert( + Collection attributes, Map response) { + return Observable.fromIterable(attributes) + .filter(attribute -> !Value.getDefaultInstance().equals(response.get(attribute.asMapKey()))) + .flatMapSingle(attribute -> this.buildAttributeMapEntry(attribute, response)) + .distinct() + .collect(CollectorUtils.immutableMapEntryCollector()); + } + + private Single> buildAttributeMapEntry( + AttributeRequest attributeRequest, Map response) { + // Uses SimpleImmutableEntry to support null values + return this.valueConverter + .convert( + response.get(attributeRequest.asMapKey()), + attributeRequest.attributeExpressionAssociation().attribute()) + .map( + value -> + new SimpleImmutableEntry<>( + attributeRequest.attributeExpressionAssociation().value(), value)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverter.java new file mode 100644 index 00000000..a6128f35 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverter.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.ColumnIdentifier; +import org.hypertrace.gateway.service.v1.common.ColumnIdentifier.Builder; + +class ColumnIdentifierConverter implements Converter { + + @Override + public Single convert(AttributeModel attribute) { + return Single.just(attribute.id()) + .map(ColumnIdentifier.newBuilder()::setColumnName) + .map(Builder::build); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierExpressionConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierExpressionConverter.java new file mode 100644 index 00000000..4d065b8b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierExpressionConverter.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import javax.inject.Inject; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Expression.Builder; + +class ColumnIdentifierExpressionConverter implements Converter { + + private final ColumnIdentifierConverter constantConverter; + + @Inject + public ColumnIdentifierExpressionConverter(final ColumnIdentifierConverter constantConverter) { + this.constantConverter = constantConverter; + } + + @Override + public Single convert(final AttributeModel model) { + return this.constantConverter + .convert(model) + .map(Expression.newBuilder()::setColumnIdentifier) + .map(Builder::build); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/FilterConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/FilterConverter.java new file mode 100644 index 00000000..fcc66305 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/FilterConverter.java @@ -0,0 +1,59 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.Operator; + +class FilterConverter + implements Converter>, Filter> { + + private final AttributeExpressionConverter attributeExpressionConverter; + private final OperatorConverter operatorConverter; + private final LiteralConstantExpressionConverter literalConstantExpressionConverter; + + @Inject + FilterConverter( + AttributeExpressionConverter attributeExpressionConverter, + OperatorConverter operatorConverter, + LiteralConstantExpressionConverter literalConstantExpressionConverter) { + this.attributeExpressionConverter = attributeExpressionConverter; + this.operatorConverter = operatorConverter; + this.literalConstantExpressionConverter = literalConstantExpressionConverter; + } + + @Override + public Single convert(Collection> filters) { + if (filters.isEmpty()) { + return Single.just(Filter.getDefaultInstance()); + } + + return Observable.fromIterable(filters) + .flatMapSingle(this::buildFilter) + .collect(Collectors.toUnmodifiableList()) + .map( + filterList -> + Filter.newBuilder() + .setOperator(Operator.AND) + .addAllChildFilter(filterList) + .build()); + } + + private Single buildFilter(AttributeAssociation filter) { + return zip( + this.attributeExpressionConverter.convert( + AttributeAssociation.of(filter.attribute(), filter.value().keyExpression())), + this.operatorConverter.convert(filter.value().operator()), + this.literalConstantExpressionConverter.convert(filter.value().value()), + (key, operator, value) -> + Filter.newBuilder().setLhs(key).setOperator(operator).setRhs(value).build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayServiceFutureStubProvider.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayServiceFutureStubProvider.java new file mode 100644 index 00000000..55363260 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayServiceFutureStubProvider.java @@ -0,0 +1,34 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.grpc.CallCredentials; +import javax.inject.Inject; +import javax.inject.Provider; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.gateway.service.GatewayServiceGrpc; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; + +class GatewayServiceFutureStubProvider implements Provider { + + private final GraphQlServiceConfig serviceConfig; + private final CallCredentials credentials; + private final GrpcChannelRegistry channelRegistry; + + @Inject + GatewayServiceFutureStubProvider( + GraphQlServiceConfig serviceConfig, + CallCredentials credentials, + GrpcChannelRegistry channelRegistry) { + this.serviceConfig = serviceConfig; + this.credentials = credentials; + this.channelRegistry = channelRegistry; + } + + @Override + public GatewayServiceFutureStub get() { + return GatewayServiceGrpc.newFutureStub( + channelRegistry.forAddress( + serviceConfig.getGatewayServiceHost(), serviceConfig.getGatewayServicePort())) + .withCallCredentials(credentials); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayUtilsModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayUtilsModule.java new file mode 100644 index 00000000..78fa0750 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/GatewayUtilsModule.java @@ -0,0 +1,82 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderDirection; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.common.ColumnIdentifier; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.LiteralConstant; +import org.hypertrace.gateway.service.v1.common.Operator; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.common.SortOrder; +import org.hypertrace.gateway.service.v1.common.Value; + +public class GatewayUtilsModule extends AbstractModule { + + @Override + protected void configure() { + bind(Key.get( + new TypeLiteral< + BiConverter< + Collection, + Map, + Map>>() {})) + .to(AttributeMapConverter.class); + + bind(Key.get(new TypeLiteral>() {})).to(UnwrappedValueConverter.class); + bind(Key.get(new TypeLiteral>() {})) + .to(UnwrappedValueConverter.class); + bind(Key.get(new TypeLiteral, Set>>() {})) + .to(SelectionExpressionSetConverter.class); + + bind(Key.get( + new TypeLiteral< + Converter< + List>, List>>() {})) + .to(OrderByExpressionListConverter.class); + + bind(Key.get(new TypeLiteral>() {})) + .to(OperatorConverter.class); + bind(Key.get( + new TypeLiteral< + Converter>, Filter>>() {})) + .to(FilterConverter.class); + + bind(Key.get(new TypeLiteral>() {})) + .to(ColumnIdentifierConverter.class); + bind(Key.get(new TypeLiteral>() {})) + .to(ColumnIdentifierExpressionConverter.class); + bind(Key.get( + new TypeLiteral, Expression>>() {})) + .to(AttributeExpressionConverter.class); + + bind(Key.get(new TypeLiteral>() {})) + .to(LiteralConstantConverter.class); + bind(Key.get(new TypeLiteral>() {})) + .to(LiteralConstantExpressionConverter.class); + + bind(Key.get(new TypeLiteral>() {})) + .to(SortOrderConverter.class); + + bind(GatewayServiceFutureStub.class) + .toProvider(GatewayServiceFutureStubProvider.class) + .in(Singleton.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverter.java new file mode 100644 index 00000000..deec8667 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverter.java @@ -0,0 +1,144 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static java.util.stream.Collectors.toUnmodifiableList; +import static org.hypertrace.gateway.service.v1.common.ValueType.BOOL; +import static org.hypertrace.gateway.service.v1.common.ValueType.BOOLEAN_ARRAY; +import static org.hypertrace.gateway.service.v1.common.ValueType.DOUBLE; +import static org.hypertrace.gateway.service.v1.common.ValueType.DOUBLE_ARRAY; +import static org.hypertrace.gateway.service.v1.common.ValueType.LONG; +import static org.hypertrace.gateway.service.v1.common.ValueType.LONG_ARRAY; +import static org.hypertrace.gateway.service.v1.common.ValueType.STRING; +import static org.hypertrace.gateway.service.v1.common.ValueType.STRING_ARRAY; +import static org.hypertrace.gateway.service.v1.common.ValueType.TIMESTAMP; + +import io.reactivex.rxjava3.core.Single; +import java.math.BigInteger; +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.LiteralConstant; +import org.hypertrace.gateway.service.v1.common.LiteralConstant.Builder; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; + +class LiteralConstantConverter implements Converter { + @Override + public Single convert(Object from) { + return Single.just(Optional.ofNullable(from)) + .map(this::convertValue) + .map(LiteralConstant.newBuilder()::setValue) + .map(Builder::build); + } + + private Value convertValue( + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional optionalObject) { + if (optionalObject.isEmpty()) { + return Value.getDefaultInstance(); + } + Object object = optionalObject.get(); + final ValueType valueType = getValueType(object); + final Value.Builder valueBuilder = Value.newBuilder().setValueType(valueType); + + switch (valueType) { + case LONG: + return valueBuilder.setLong(((Number) object).longValue()).build(); + + case DOUBLE: + return valueBuilder.setDouble(((Number) object).doubleValue()).build(); + + case BOOL: + return valueBuilder.setBoolean((Boolean) object).build(); + + case TIMESTAMP: + return valueBuilder + .setTimestamp(Instant.from((TemporalAccessor) object).toEpochMilli()) + .build(); + + case BOOLEAN_ARRAY: + return valueBuilder + .addAllBooleanArray( + ((Collection) object) + .stream().map(obj -> (Boolean) obj).collect(toUnmodifiableList())) + .build(); + + case LONG_ARRAY: + return valueBuilder + .addAllLongArray( + ((Collection) object) + .stream().map(obj -> ((Number) obj).longValue()).collect(toUnmodifiableList())) + .build(); + + case DOUBLE_ARRAY: + return valueBuilder + .addAllDoubleArray( + ((Collection) object) + .stream() + .map(obj -> ((Number) obj).doubleValue()) + .collect(toUnmodifiableList())) + .build(); + + case STRING_ARRAY: + return valueBuilder + .addAllStringArray( + ((Collection) object) + .stream().map(String::valueOf).collect(toUnmodifiableList())) + .build(); + } + + return valueBuilder.setString(String.valueOf(object)).build(); + } + + private boolean assignableToAnyOfClasses(Class classToCheck, Class... classesAllowed) { + return Arrays.stream(classesAllowed) + .anyMatch(allowedClass -> allowedClass.isAssignableFrom(classToCheck)); + } + + private ValueType getValueType(final Object object) { + if (this.assignableToAnyOfClasses( + object.getClass(), Long.class, Integer.class, BigInteger.class)) { + return LONG; + } + if (this.assignableToAnyOfClasses(object.getClass(), Number.class)) { + return DOUBLE; + } + if (this.assignableToAnyOfClasses(object.getClass(), Boolean.class)) { + return BOOL; + } + // todo handle Instant type object + if (this.assignableToAnyOfClasses(object.getClass(), TemporalAccessor.class)) { + return TIMESTAMP; + } + + if (this.assignableToAnyOfClasses(object.getClass(), Collection.class)) { + return getCollectionType(object); + } + + return STRING; + } + + private ValueType getCollectionType(final Object object) { + final Collection collection = (Collection) object; + if (collection.isEmpty()) { + return STRING_ARRAY; + } + + final Object first = collection.iterator().next(); + final ValueType baseType = getValueType(first); + + switch (baseType) { + case BOOL: + return BOOLEAN_ARRAY; + + case LONG: + return LONG_ARRAY; + + case DOUBLE: + return DOUBLE_ARRAY; + } + + return STRING_ARRAY; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverter.java new file mode 100644 index 00000000..e5bbe0b4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverter.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Expression.Builder; + +class LiteralConstantExpressionConverter implements Converter { + + private final LiteralConstantConverter constantConverter; + + @Inject + LiteralConstantExpressionConverter(LiteralConstantConverter constantConverter) { + this.constantConverter = constantConverter; + } + + @Override + public Single convert(Object from) { + return this.constantConverter + .convert(from) + .map(Expression.newBuilder()::setLiteral) + .map(Builder::build); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OperatorConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OperatorConverter.java new file mode 100644 index 00000000..79fbd2f9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OperatorConverter.java @@ -0,0 +1,47 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import java.util.UnknownFormatConversionException; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Operator; + +class OperatorConverter implements Converter { + + @Override + public Single convert(FilterOperatorType filterOperatorType) { + switch (filterOperatorType) { + case LESS_THAN: + return Single.just(Operator.LT); + case LESS_THAN_OR_EQUAL_TO: + return Single.just(Operator.LE); + case GREATER_THAN: + return Single.just(Operator.GT); + case GREATER_THAN_OR_EQUAL_TO: + return Single.just(Operator.GE); + case EQUALS: + return Single.just(Operator.EQ); + case NOT_EQUALS: + return Single.just(Operator.NEQ); + case IN: + return Single.just(Operator.IN); + case NOT_IN: + return Single.just(Operator.NOT_IN); + case LIKE: + return Single.just(Operator.LIKE); + case CONTAINS_KEY: + return Single.just(Operator.CONTAINS_KEY); + case NOT_CONTAINS_KEY: + return Single.just(Operator.NOT_CONTAINS_KEY); + case CONTAINS_KEY_VALUE: + return Single.just(Operator.CONTAINS_KEYVALUE); + case CONTAINS_KEY_LIKE: + return Single.just(Operator.CONTAINS_KEY_LIKE); + default: + return Single.error( + new UnknownFormatConversionException( + String.format( + "Unable to convert unknown operator '%s'", filterOperatorType.name()))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OrderByExpressionListConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OrderByExpressionListConverter.java new file mode 100644 index 00000000..7c637c67 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/OrderByExpressionListConverter.java @@ -0,0 +1,49 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; + +class OrderByExpressionListConverter + implements Converter>, List> { + + private final AttributeExpressionConverter attributeExpressionConverter; + private final SortOrderConverter sortOrderConverter; + + @Inject + OrderByExpressionListConverter( + AttributeExpressionConverter attributeExpressionConverter, + SortOrderConverter sortOrderConverter) { + this.attributeExpressionConverter = attributeExpressionConverter; + this.sortOrderConverter = sortOrderConverter; + } + + @Override + public Single> convert(List> orders) { + return Observable.fromIterable(orders) + .flatMapSingle(this::buildOrderByExpression) + .collect(Collectors.toUnmodifiableList()); + } + + private Single buildOrderByExpression( + AttributeAssociation orderArgument) { + return zip( + this.sortOrderConverter.convert(orderArgument.value().direction()), + this.attributeExpressionConverter.convert( + AttributeAssociation.of( + orderArgument.attribute(), orderArgument.value().resolvedKeyExpression())), + (sortOrder, columnExpression) -> + OrderByExpression.newBuilder() + .setOrder(sortOrder) + .setExpression(columnExpression) + .build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SelectionExpressionSetConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SelectionExpressionSetConverter.java new file mode 100644 index 00000000..aadd4f8a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SelectionExpressionSetConverter.java @@ -0,0 +1,38 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import com.google.common.collect.ImmutableSet; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Expression.Builder; + +class SelectionExpressionSetConverter + implements Converter, Set> { + + private final AttributeExpressionConverter attributeExpressionConverter; + + @Inject + SelectionExpressionSetConverter(AttributeExpressionConverter attributeExpressionConverter) { + this.attributeExpressionConverter = attributeExpressionConverter; + } + + @Override + public Single> convert(Collection attributeRequests) { + return Observable.fromIterable(attributeRequests) + .flatMapSingle(this::buildAliasedSelectionExpression) + .collectInto(ImmutableSet.builder(), ImmutableSet.Builder::add) + .map(ImmutableSet.Builder::build); + } + + private Single buildAliasedSelectionExpression(AttributeRequest attributeRequest) { + return this.attributeExpressionConverter + .convert(attributeRequest.attributeExpressionAssociation()) + .map(Expression::toBuilder) + .map(Builder::build); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SortOrderConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SortOrderConverter.java new file mode 100644 index 00000000..3ceae53c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/SortOrderConverter.java @@ -0,0 +1,23 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderDirection; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.SortOrder; + +class SortOrderConverter implements Converter { + + @Override + public Single convert(OrderDirection orderDirection) { + switch (orderDirection) { + case ASC: + return Single.just(SortOrder.ASC); + case DESC: + return Single.just(SortOrder.DESC); + default: + return Single.error( + new UnsupportedOperationException( + String.format("Cannot convert sort order for unknown value '%s'", orderDirection))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverter.java new file mode 100644 index 00000000..0f064c52 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/main/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverter.java @@ -0,0 +1,70 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import io.reactivex.rxjava3.core.Single; +import java.time.Instant; +import java.util.Optional; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; + +class UnwrappedValueConverter + implements Converter, BiConverter { + + @Override + public Single convert(Value from) { + Value value = Optional.ofNullable(from).orElse(Value.getDefaultInstance()); + switch (value.getValueType()) { + case STRING: + return Single.just(value.getString()); + case BOOL: + return Single.just(value.getBoolean()); + case LONG: + return Single.just(value.getLong()); + case DOUBLE: + return Single.just(value.getDouble()); + case TIMESTAMP: + try { + return Single.just(Instant.ofEpochMilli(value.getTimestamp())); + } catch (Throwable t) { + return Single.error(t); + } + case STRING_MAP: + return Single.just(value.getStringMapMap()); + case STRING_ARRAY: + return Single.just(value.getStringArrayList()); + case LONG_ARRAY: + case DOUBLE_ARRAY: + case BOOLEAN_ARRAY: + case UNSET: + case UNRECOGNIZED: + default: + return Single.error( + new UnsupportedOperationException( + String.format( + "Cannot convert value for unknown value type '%s'", + value.getValueType().name()))); + } + } + + @Override + public Single convert(Value from, AttributeModel attributeModel) { + Value value = Optional.ofNullable(from).orElse(Value.getDefaultInstance()); + if (ValueType.TIMESTAMP.equals(value.getValueType())) { + return handleTimestamp(value, attributeModel); + } + return convert(from); + } + + private Single handleTimestamp(Value value, AttributeModel attributeModel) { + try { + if ("ns".equals(attributeModel.units())) { + return Single.just(Instant.ofEpochSecond(0, value.getTimestamp())); + } + return Single.just(Instant.ofEpochMilli(value.getTimestamp())); + } catch (Throwable t) { + return Single.error(t); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverterTest.java new file mode 100644 index 00000000..49d63259 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/AttributeExpressionConverterTest.java @@ -0,0 +1,59 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AttributeExpressionConverterTest { + @Mock AttributeModel mockAttribute; + + @Test + void convertsAttributeExpressionWithSubpath() { + org.hypertrace.gateway.service.v1.common.AttributeExpression expectedAttributeExpression = + org.hypertrace.gateway.service.v1.common.AttributeExpression.newBuilder() + .setAttributeId("EVENT.tags") + .setAlias("tags.spantag") + .setSubpath("spantag") + .build(); + + when(mockAttribute.id()).thenReturn("EVENT.tags"); + AttributeExpressionConverter attributeExpressionConverter = new AttributeExpressionConverter(); + + assertEquals( + Expression.newBuilder().setAttributeExpression(expectedAttributeExpression).build(), + attributeExpressionConverter + .convert( + AttributeAssociation.of( + this.mockAttribute, new AttributeExpression("tags", "spantag"))) + .blockingGet()); + } + + @Test + void convertsAttributeExpressionWithoutSubpath() { + org.hypertrace.gateway.service.v1.common.AttributeExpression expectedAttributeExpression = + org.hypertrace.gateway.service.v1.common.AttributeExpression.newBuilder() + .setAttributeId("EVENT.tags") + .setAlias("tags") + .build(); + + when(mockAttribute.id()).thenReturn("EVENT.tags"); + AttributeExpressionConverter attributeExpressionConverter = new AttributeExpressionConverter(); + + assertEquals( + Expression.newBuilder().setAttributeExpression(expectedAttributeExpression).build(), + attributeExpressionConverter + .convert( + AttributeAssociation.of( + this.mockAttribute, AttributeExpression.forAttributeKey("tags"))) + .blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverterTest.java new file mode 100644 index 00000000..a4b8de0a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/ColumnIdentifierConverterTest.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.gateway.service.v1.common.ColumnIdentifier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ColumnIdentifierConverterTest { + @Mock AttributeModel mockAttribute; + + @Test + void convertsColumnBasedOnAttributeId() { + when(mockAttribute.id()).thenReturn("expectedIdentifier"); + + assertEquals( + ColumnIdentifier.newBuilder().setColumnName("expectedIdentifier").build(), + new ColumnIdentifierConverter().convert(this.mockAttribute).blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverterTest.java new file mode 100644 index 00000000..fe4fadf3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantConverterTest.java @@ -0,0 +1,102 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.List; +import org.hypertrace.gateway.service.v1.common.LiteralConstant; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.junit.jupiter.api.Test; + +class LiteralConstantConverterTest { + + @Test + void convertsValuesOfVariousTypes() { + LiteralConstantConverter converter = new LiteralConstantConverter(); + + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.STRING).setString("foo")) + .build(), + converter.convert("foo").blockingGet()); + + assertEquals( + LiteralConstant.newBuilder() + .setValue( + Value.newBuilder() + .setValueType(ValueType.STRING_ARRAY) + .addAllStringArray(List.of("foo", "bar"))) + .build(), + converter.convert(List.of("foo", "bar")).blockingGet()); + + assertEquals( + LiteralConstant.newBuilder() + .setValue( + Value.newBuilder() + .setValueType(ValueType.BOOLEAN_ARRAY) + .addAllBooleanArray(List.of(true, false))) + .build(), + converter.convert(List.of(true, false)).blockingGet()); + + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.LONG).setLong(100)) + .build(), + converter.convert(100L).blockingGet()); + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.LONG).setLong(100)) + .build(), + converter.convert(100).blockingGet()); + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.LONG).setLong(100)) + .build(), + converter.convert(BigInteger.valueOf(100)).blockingGet()); + + assertEquals( + LiteralConstant.newBuilder() + .setValue( + Value.newBuilder() + .setValueType(ValueType.DOUBLE) + .setDouble( + BigDecimal.valueOf(53.4f).doubleValue())) // careful of double epsilon + .build(), + converter.convert(BigDecimal.valueOf(53.4f)).blockingGet()); + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.DOUBLE).setDouble(53.4f)) + .build(), + converter.convert(53.4f).blockingGet()); + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.DOUBLE).setDouble(53.4)) + .build(), + converter.convert(53.4d).blockingGet()); + + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.BOOL).setBoolean(true)) + .build(), + converter.convert(true).blockingGet()); + + long timestamp = System.currentTimeMillis(); + + assertEquals( + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setValueType(ValueType.TIMESTAMP).setTimestamp(timestamp)) + .build(), + converter.convert(Instant.ofEpochMilli(timestamp)).blockingGet()); + } + + @Test + void convertsNullValue() { + LiteralConstantConverter converter = new LiteralConstantConverter(); + assertEquals( + LiteralConstant.newBuilder().setValue(Value.getDefaultInstance()).build(), + converter.convert(null).blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverterTest.java new file mode 100644 index 00000000..d03ec079 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/LiteralConstantExpressionConverterTest.java @@ -0,0 +1,38 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.LiteralConstant; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LiteralConstantExpressionConverterTest { + @Mock LiteralConstantConverter mockLiteralConstantConverter; + + @Test + void convertsLiteralExpression() { + LiteralConstant expectedLiteral = + LiteralConstant.newBuilder() + .setValue(Value.newBuilder().setString("foo").setValueType(ValueType.STRING)) + .build(); + + LiteralConstantExpressionConverter columnIdentifierExpressionConverter = + new LiteralConstantExpressionConverter(this.mockLiteralConstantConverter); + + when(this.mockLiteralConstantConverter.convert(eq("foo"))) + .thenReturn(Single.just(expectedLiteral)); + + assertEquals( + Expression.newBuilder().setLiteral(expectedLiteral).build(), + columnIdentifierExpressionConverter.convert("foo").blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverterTest.java new file mode 100644 index 00000000..f013ae1a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-gateway-service-utils/src/test/java/org/hypertrace/core/graphql/utils/gateway/UnwrappedValueConverterTest.java @@ -0,0 +1,35 @@ +package org.hypertrace.core.graphql.utils.gateway; + +import java.time.Duration; +import java.time.Instant; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UnwrappedValueConverterTest { + + @Mock AttributeModel attributeModel; + + @Test + void testTimestampConversion() { + UnwrappedValueConverter unwrappedValueConverter = new UnwrappedValueConverter(); + long millis = System.currentTimeMillis(); + Value value = + Value.newBuilder() + .setValueType(ValueType.TIMESTAMP) + .setTimestamp(Duration.ofMillis(millis).toNanos()) + .build(); + Mockito.when(attributeModel.units()).thenReturn("ns"); + + Instant instant = + (Instant) unwrappedValueConverter.convert(value, attributeModel).blockingGet(); + Assertions.assertEquals(Instant.ofEpochMilli(millis), instant); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/build.gradle.kts new file mode 100644 index 00000000..0cf89b3b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(commonLibs.grpc.api) + api(commonLibs.grpc.core) + api(commonLibs.grpc.stub) + api(projects.hypertraceCoreGraphqlContext) + api(commonLibs.hypertrace.grpcutils.context) + + implementation(commonLibs.hypertrace.grpcutils.client) + implementation(commonLibs.grpc.context) + implementation(commonLibs.rxjava3) + implementation(commonLibs.slf4j2.api) + implementation(projects.hypertraceCoreGraphqlSpi) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) + + testRuntimeOnly(commonLibs.grpc.netty) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/gradle.lockfile new file mode 100644 index 00000000..6f4b8ed5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/gradle.lockfile @@ -0,0 +1,82 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-netty:1.60.0=testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.108.Final=testRuntimeClasspath +io.netty:netty-codec-http2:4.1.108.Final=testRuntimeClasspath +io.netty:netty-codec-http:4.1.108.Final=testRuntimeClasspath +io.netty:netty-codec-socks:4.1.108.Final=testRuntimeClasspath +io.netty:netty-codec:4.1.108.Final=testRuntimeClasspath +io.netty:netty-common:4.1.108.Final=testRuntimeClasspath +io.netty:netty-handler-proxy:4.1.108.Final=testRuntimeClasspath +io.netty:netty-handler:4.1.108.Final=testRuntimeClasspath +io.netty:netty-resolver:4.1.108.Final=testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.108.Final=testRuntimeClasspath +io.netty:netty-transport:4.1.108.Final=testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilder.java new file mode 100644 index 00000000..29f1b6dc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilder.java @@ -0,0 +1,54 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Observable; +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import javax.inject.Inject; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.grpcutils.context.RequestContext; + +class DefaultGraphQlGrpcContextBuilder implements GraphQlGrpcContextBuilder { + + private final PlatformGrpcContextBuilder platformGrpcContextBuilder; + + @Inject + DefaultGraphQlGrpcContextBuilder(PlatformGrpcContextBuilder platformGrpcContextBuilder) { + this.platformGrpcContextBuilder = platformGrpcContextBuilder; + } + + @Override + public GraphQlGrpcContext build(GraphQlRequestContext requestContext) { + return new DefaultGraphQlGrpcContext(requestContext, this.platformGrpcContextBuilder); + } + + private static final class DefaultGraphQlGrpcContext implements GraphQlGrpcContext { + + private final RequestContext grpcContext; + + private DefaultGraphQlGrpcContext( + GraphQlRequestContext requestContext, + PlatformGrpcContextBuilder platformGrpcContextBuilder) { + this.grpcContext = platformGrpcContextBuilder.build(requestContext); + } + + @Override + public TResp callInContext(Callable callable) { + return this.grpcContext.call(callable); + } + + @Override + public void runInContext(Runnable runnable) { + this.grpcContext.run(runnable); + } + + @Override + public Observable streamInContext( + Consumer> requestExecutor) { + return Observable.create( + emitter -> + this.runInContext( + () -> requestExecutor.accept(new StreamingClientResponseObserver<>(emitter)))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistry.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistry.java new file mode 100644 index 00000000..fe5acba3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistry.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import javax.inject.Inject; +import org.hypertrace.core.grpcutils.client.GrpcChannelConfig; + +class DefaultGrpcChannelRegistry implements GrpcChannelRegistry { + + private final org.hypertrace.core.grpcutils.client.GrpcChannelRegistry delegate; + + @Inject + DefaultGrpcChannelRegistry(org.hypertrace.core.grpcutils.client.GrpcChannelRegistry delegate) { + this.delegate = delegate; + } + + @Override + public ManagedChannel forAddress(String host, int port) { + return this.delegate.forPlaintextAddress(host, port); + } + + @Override + public Channel forAddress(String host, int port, GrpcChannelConfig channelConfig) { + return this.delegate.forPlaintextAddress(host, port, channelConfig); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContext.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContext.java new file mode 100644 index 00000000..337d1e3b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContext.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import io.grpc.stub.StreamObserver; +import io.reactivex.rxjava3.core.Observable; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +@Deprecated +public interface GraphQlGrpcContext { + + TResp callInContext(Callable callable); + + void runInContext(Runnable runnable); + + Observable streamInContext( + Consumer> requestExecutor); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContextBuilder.java new file mode 100644 index 00000000..fcaa7726 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcContextBuilder.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +/** + * @deprecated Use {@link GrpcContextBuilder#build(GraphQlRequestContext)} instead + */ +@Deprecated +public interface GraphQlGrpcContextBuilder { + GraphQlGrpcContext build(GraphQlRequestContext requestContext); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcModule.java new file mode 100644 index 00000000..1e32e347 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GraphQlGrpcModule.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.hypertrace.core.grpcutils.client.RequestContextClientCallCredsProviderFactory.getClientCallCredsProvider; + +import com.google.inject.AbstractModule; +import io.grpc.CallCredentials; + +public class GraphQlGrpcModule extends AbstractModule { + + @Override + protected void configure() { + bind(CallCredentials.class).toInstance(getClientCallCredsProvider().get()); + bind(GraphQlGrpcContextBuilder.class).to(DefaultGraphQlGrpcContextBuilder.class); + bind(GrpcChannelRegistry.class).to(DefaultGrpcChannelRegistry.class); + bind(GrpcContextBuilder.class).to(PlatformGrpcContextBuilder.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcChannelRegistry.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcChannelRegistry.java new file mode 100644 index 00000000..2bb22507 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcChannelRegistry.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import io.grpc.Channel; +import org.hypertrace.core.grpcutils.client.GrpcChannelConfig; + +public interface GrpcChannelRegistry { + Channel forAddress(String host, int port); + + Channel forAddress(String host, int port, GrpcChannelConfig channelConfig); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcContextBuilder.java new file mode 100644 index 00000000..26373636 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/GrpcContextBuilder.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import java.util.Optional; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.grpcutils.context.RequestContext; + +public interface GrpcContextBuilder { + RequestContext build(GraphQlRequestContext requestContext); + + Optional tryRestore(RequestContext requestContext); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilder.java new file mode 100644 index 00000000..6b970775 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilder.java @@ -0,0 +1,75 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.hypertrace.core.grpcutils.context.RequestContextConstants.AUTHORIZATION_HEADER; +import static org.hypertrace.core.grpcutils.context.RequestContextConstants.REQUEST_ID_HEADER_KEY; +import static org.hypertrace.core.grpcutils.context.RequestContextConstants.TENANT_ID_HEADER_KEY; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.inject.Singleton; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.grpcutils.context.RequestContext; + +@Singleton +class PlatformGrpcContextBuilder implements GrpcContextBuilder { + + private final Cache contextCache = + CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(1)).maximumSize(1000).build(); + + @Override + public RequestContext build(GraphQlRequestContext requestContext) { + this.contextCache.put(requestContext.getRequestId(), requestContext); + Map grpcHeaders = + this.mergeMaps( + requestContext.getTracingContextHeaders(), + this.flattenOptionalMap( + Map.of( + AUTHORIZATION_HEADER, this.extractAuthorizationHeader(requestContext), + TENANT_ID_HEADER_KEY, this.extractTenantId(requestContext), + REQUEST_ID_HEADER_KEY, Optional.of(requestContext.getRequestId())))); + + return this.build(grpcHeaders); + } + + @Override + public Optional tryRestore(RequestContext requestContext) { + return requestContext.getRequestId().map(this.contextCache::getIfPresent); + } + + private RequestContext build(@Nonnull Map headers) { + RequestContext platformContext = new RequestContext(); + headers.forEach(platformContext::add); + return platformContext; + } + + private Optional extractAuthorizationHeader(GraphQlRequestContext requestContext) { + return requestContext.getAuthorizationHeader().filter(header -> header.length() > 0); + } + + private Optional extractTenantId(GraphQlRequestContext requestContext) { + return requestContext.getTenantId(); + } + + private Map flattenOptionalMap(Map> optionalMap) { + return optionalMap.entrySet().stream() + .map(entry -> entry.getValue().map(value -> Map.entry(entry.getKey(), value))) + .flatMap(Optional::stream) + .collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue)); + } + + @SafeVarargs + private Map mergeMaps(Map... maps) { + return Arrays.stream(maps) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserver.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserver.java new file mode 100644 index 00000000..d3f270d0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/main/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserver.java @@ -0,0 +1,46 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import io.reactivex.rxjava3.core.ObservableEmitter; + +class StreamingClientResponseObserver implements ClientResponseObserver { + + private final ObservableEmitter emitter; + private ClientCallStreamObserver requestStream; + + StreamingClientResponseObserver(final ObservableEmitter emitter) { + this.emitter = emitter; + this.emitter.setCancellable( + () -> + this.requestStream.cancel( + "StreamingClientResponseObserver cancelling after emitter disposed", null)); + } + + @Override + public void beforeStart(ClientCallStreamObserver requestStream) { + this.requestStream = requestStream; + } + + @Override + public void onNext(RespT value) { + if (!this.emitter.isDisposed()) { + this.emitter.onNext(value); + } + } + + @Override + public void onError(Throwable t) { + if (!this.emitter.isDisposed()) { + this.emitter.onError(t); + } + } + + @Override + public void onCompleted() { + // This shouldn't generally happen - either an error or next response + if (!this.emitter.isDisposed()) { + emitter.onComplete(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilderTest.java new file mode 100644 index 00000000..fb57840e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGraphQlGrpcContextBuilderTest.java @@ -0,0 +1,58 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.observers.TestObserver; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.grpcutils.context.RequestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultGraphQlGrpcContextBuilderTest { + @Mock PlatformGrpcContextBuilder mockPlatformGrpcContextBuilder; + @Mock GraphQlRequestContext mockRequestContext; + + @Mock(answer = Answers.CALLS_REAL_METHODS) + RequestContext mockPlatformContext; + + private GraphQlGrpcContextBuilder builder; + + @BeforeEach + void beforeEach() { + this.builder = new DefaultGraphQlGrpcContextBuilder(this.mockPlatformGrpcContextBuilder); + when(this.mockPlatformGrpcContextBuilder.build(this.mockRequestContext)) + .thenReturn(this.mockPlatformContext); + } + + @Test + void addsGrpcContextToStreamedRequest() { + var context = this.builder.build(this.mockRequestContext); + var testObserver = new TestObserver<>(); + context + .streamInContext( + streamObserver -> { + streamObserver.onNext(RequestContext.CURRENT.get()); + streamObserver.onCompleted(); + }) + .subscribe(testObserver); + + testObserver.assertValue(this.mockPlatformContext); + testObserver.assertComplete(); + } + + @Test + void propagatesErrorsFromStreamedRequest() { + var context = this.builder.build(this.mockRequestContext); + var testObserver = new TestObserver<>(); + context + .streamInContext(streamObserver -> streamObserver.onError(new IllegalStateException())) + .subscribe(testObserver); + + testObserver.assertError(IllegalStateException.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistryTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistryTest.java new file mode 100644 index 00000000..762f085c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/DefaultGrpcChannelRegistryTest.java @@ -0,0 +1,35 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import io.grpc.Channel; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultGrpcChannelRegistryTest { + DefaultGrpcChannelRegistry channelRegistry; + + @BeforeEach + void beforeEach() { + this.channelRegistry = new DefaultGrpcChannelRegistry(new GrpcChannelRegistry()); + } + + @Test + void createsNewChannelsAsRequested() { + assertNotNull(this.channelRegistry.forAddress("foo", 1000)); + } + + @Test + void reusesChannelsForDuplicateRequests() { + Channel firstChannel = this.channelRegistry.forAddress("foo", 1000); + assertSame(firstChannel, this.channelRegistry.forAddress("foo", 1000)); + assertNotSame(firstChannel, this.channelRegistry.forAddress("foo", 1001)); + assertNotSame(firstChannel, this.channelRegistry.forAddress("bar", 1000)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilderTest.java new file mode 100644 index 00000000..20ef7a77 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/PlatformGrpcContextBuilderTest.java @@ -0,0 +1,73 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.hypertrace.core.grpcutils.context.RequestContextConstants.REQUEST_ID_HEADER_KEY; +import static org.hypertrace.core.grpcutils.context.RequestContextConstants.TENANT_ID_HEADER_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Optional; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.grpcutils.context.RequestContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PlatformGrpcContextBuilderTest { + + @Mock GraphQlRequestContext mockRequestContext; + + private final PlatformGrpcContextBuilder builder = new PlatformGrpcContextBuilder(); + + @Test + void addsTenantIdToContext() { + when(this.mockRequestContext.getTenantId()).thenReturn(Optional.of("tenant id")); + when(this.mockRequestContext.getRequestId()).thenReturn("request id"); + assertEquals( + Optional.of("tenant id"), this.builder.build(this.mockRequestContext).getTenantId()); + } + + @Test + void addsTracingHeadersToContext() { + when(this.mockRequestContext.getTenantId()).thenReturn(Optional.of("tenant id")); + when(this.mockRequestContext.getRequestId()).thenReturn("request id"); + when(this.mockRequestContext.getTracingContextHeaders()) + .thenReturn(Map.of("traceid", "traceid value")); + this.builder.build(this.mockRequestContext); + assertEquals( + Map.of( + TENANT_ID_HEADER_KEY, + "tenant id", + REQUEST_ID_HEADER_KEY, + "request id", + "traceid", + "traceid value"), + this.builder.build(this.mockRequestContext).getRequestHeaders()); + } + + @Test + void passesAuthHeaderToPlatformContextIfPresent() { + when(this.mockRequestContext.getRequestId()).thenReturn("request id"); + assertFalse( + this.builder + .build(this.mockRequestContext) + .getRequestHeaders() + .containsKey("authorization")); + + when(this.mockRequestContext.getAuthorizationHeader()).thenReturn(Optional.of("auth header")); + assertEquals( + "auth header", + this.builder.build(this.mockRequestContext).getRequestHeaders().get("authorization")); + } + + @Test + void testRestoreContext() { + when(this.mockRequestContext.getRequestId()).thenReturn("request id"); + RequestContext resultContext = this.builder.build(this.mockRequestContext); + assertEquals(Optional.empty(), this.builder.tryRestore(RequestContext.forTenantId("other"))); + assertEquals(Optional.of(this.mockRequestContext), this.builder.tryRestore(resultContext)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserverTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserverTest.java new file mode 100644 index 00000000..2ded42bf --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-grpc-utils/src/test/java/org/hypertrace/core/graphql/utils/grpc/StreamingClientResponseObserverTest.java @@ -0,0 +1,65 @@ +package org.hypertrace.core.graphql.utils.grpc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import io.grpc.stub.ClientCallStreamObserver; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.observers.TestObserver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class StreamingClientResponseObserverTest { + + @Mock ClientCallStreamObserver requestStreamObserver; + + private StreamingClientResponseObserver responseObserver; + private TestObserver testObserver; + + @BeforeEach + void beforeEach() { + Observable observable = + Observable.create( + observer -> this.responseObserver = new StreamingClientResponseObserver<>(observer)); + this.testObserver = new TestObserver<>(); + observable.subscribe(this.testObserver); + this.responseObserver.beforeStart(this.requestStreamObserver); + } + + @Test + void returnsValues() { + this.responseObserver.onNext("first"); + this.testObserver.assertValue("first"); + + this.responseObserver.onNext("second"); + this.testObserver.assertValueAt(1, "second"); + this.testObserver.assertNotComplete(); + + verifyNoInteractions(requestStreamObserver); + } + + @Test + void propagatesExceptionOnError() { + Throwable t = new Exception("error"); + this.responseObserver.onError(t); + + this.testObserver.assertError(t); + this.testObserver.assertNoValues(); + verify(requestStreamObserver).cancel(any(), any()); + } + + @Test + void propagatesCompletion() { + this.responseObserver.onCompleted(); + + this.testObserver.assertComplete(); + this.testObserver.assertNoValues(); + + verify(requestStreamObserver).cancel(any(), any()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-impl/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-impl/build.gradle.kts new file mode 100644 index 00000000..944a8bd1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-impl/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(projects.hypertraceCoreGraphqlSpi) + api(localLibs.graphql.servlet) + api(commonLibs.hypertrace.grpcutils.client) + + implementation(projects.hypertraceCoreGraphqlSchemaRegistry) + implementation(projects.hypertraceCoreGraphqlContext) + implementation(projects.hypertraceCoreGraphqlDeserialization) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + implementation(projects.hypertraceCoreGraphqlSchemaUtils) + implementation(projects.hypertraceCoreGraphqlGatewayServiceUtils) + implementation(projects.hypertraceCoreGraphqlAttributeStore) + implementation(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlMetadataSchema) + implementation(projects.hypertraceCoreGraphqlSpanSchema) + implementation(projects.hypertraceCoreGraphqlTraceSchema) + implementation(projects.hypertraceCoreGraphqlAttributeScope) + implementation(projects.hypertraceCoreGraphqlRxUtils) + implementation(projects.hypertraceCoreGraphqlLogEventSchema) + implementation(projects.hypertraceCoreGraphqlRequestTransformation) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.guice) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-impl/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-impl/gradle.lockfile new file mode 100644 index 00000000..dc6b4860 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-impl/gradle.lockfile @@ -0,0 +1,84 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java-util:3.24.1=runtimeClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-proto:1.1.0-alpha=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.12.0=runtimeClasspath,testRuntimeClasspath +org.apache.commons:commons-text:1.10.0=runtimeClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlFactory.java b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlFactory.java new file mode 100644 index 00000000..7b368d5d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlFactory.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.impl; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import graphql.kickstart.servlet.GraphQLConfiguration; +import graphql.schema.GraphQLSchema; +import org.hypertrace.core.graphql.context.GraphQlRequestContextBuilder; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.spi.lifecycle.GraphQlServiceLifecycle; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; + +public class GraphQlFactory { + public static GraphQLConfiguration build( + GraphQlServiceConfig serviceConfig, + GraphQlEndpointConfig endpointConfig, + GraphQlServiceLifecycle lifecycle, + GrpcChannelRegistry grpcChannelRegistry) { + final Injector injector = + Guice.createInjector( + new GraphQlModule(serviceConfig, endpointConfig, lifecycle, grpcChannelRegistry)); + + return GraphQLConfiguration.with(injector.getInstance(GraphQLSchema.class)) + .with(injector.getInstance(GraphQlRequestContextBuilder.class)) + .asyncTimeout(endpointConfig.getTimeout().toMillis()) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlModule.java new file mode 100644 index 00000000..a636d2c9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/main/java/org/hypertrace/core/graphql/impl/GraphQlModule.java @@ -0,0 +1,66 @@ +package org.hypertrace.core.graphql.impl; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.attributes.AttributeStoreModule; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeModule; +import org.hypertrace.core.graphql.common.schema.CommonSchemaModule; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeUtilsModule; +import org.hypertrace.core.graphql.context.GraphQlRequestContextModule; +import org.hypertrace.core.graphql.deserialization.GraphQlDeserializationRegistryModule; +import org.hypertrace.core.graphql.log.event.LogEventSchemaModule; +import org.hypertrace.core.graphql.metadata.MetadataSchemaModule; +import org.hypertrace.core.graphql.request.transformation.RequestTransformationModule; +import org.hypertrace.core.graphql.rx.RxUtilModule; +import org.hypertrace.core.graphql.schema.registry.GraphQlSchemaRegistryModule; +import org.hypertrace.core.graphql.span.SpanSchemaModule; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.spi.lifecycle.GraphQlServiceLifecycle; +import org.hypertrace.core.graphql.trace.TraceSchemaModule; +import org.hypertrace.core.graphql.utils.gateway.GatewayUtilsModule; +import org.hypertrace.core.graphql.utils.grpc.GraphQlGrpcModule; +import org.hypertrace.core.graphql.utils.schema.SchemaUtilsModule; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; + +class GraphQlModule extends AbstractModule { + + private final GraphQlServiceConfig serviceConfig; + private final GraphQlEndpointConfig endpointConfig; + private final GraphQlServiceLifecycle lifecycle; + private final GrpcChannelRegistry grpcChannelRegistry; + + public GraphQlModule( + final GraphQlServiceConfig serviceConfig, + final GraphQlEndpointConfig endpointConfig, + final GraphQlServiceLifecycle lifecycle, + final GrpcChannelRegistry grpcChannelRegistry) { + this.serviceConfig = serviceConfig; + this.endpointConfig = endpointConfig; + this.lifecycle = lifecycle; + this.grpcChannelRegistry = grpcChannelRegistry; + } + + @Override + protected void configure() { + bind(GraphQlServiceConfig.class).toInstance(this.serviceConfig); + bind(GraphQlEndpointConfig.class).toInstance(this.endpointConfig); + bind(GraphQlServiceLifecycle.class).toInstance(this.lifecycle); + bind(GrpcChannelRegistry.class).toInstance(this.grpcChannelRegistry); + install(new GraphQlRequestContextModule()); + install(new GraphQlGrpcModule()); + install(new GraphQlSchemaRegistryModule()); + install(new GraphQlDeserializationRegistryModule()); + install(new CommonSchemaModule()); + install(new GatewayUtilsModule()); + install(new SchemaUtilsModule()); + install(new AttributeStoreModule()); + install(new AttributeUtilsModule()); + install(new HypertraceCoreAttributeScopeModule()); + install(new MetadataSchemaModule()); + install(new SpanSchemaModule()); + install(new TraceSchemaModule()); + install(new RxUtilModule()); + install(new LogEventSchemaModule()); + install(new RequestTransformationModule()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/test/java/org/hypertrace/core/graphql/impl/GraphQlModuleTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/test/java/org/hypertrace/core/graphql/impl/GraphQlModuleTest.java new file mode 100644 index 00000000..4677ffec --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-impl/src/test/java/org/hypertrace/core/graphql/impl/GraphQlModuleTest.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; + +import com.google.inject.Guice; +import graphql.schema.GraphQLSchema; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.spi.lifecycle.GraphQlServiceLifecycle; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; +import org.junit.jupiter.api.Test; + +public class GraphQlModuleTest { + + @Test + public void testResolveBindings() { + assertDoesNotThrow( + () -> + Guice.createInjector( + new GraphQlModule( + mock(GraphQlServiceConfig.class), + mock(GraphQlEndpointConfig.class), + mock(GraphQlServiceLifecycle.class), + mock(GrpcChannelRegistry.class))) + .getAllBindings()); + } + + @Test + public void testResolveSchema() { + assertDoesNotThrow( + () -> + Guice.createInjector( + new GraphQlModule( + mock(GraphQlServiceConfig.class), + mock(GraphQlEndpointConfig.class), + mock(GraphQlServiceLifecycle.class), + mock(GrpcChannelRegistry.class))) + .getInstance(GraphQLSchema.class)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/build.gradle.kts new file mode 100644 index 00000000..73805999 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(projects.hypertraceCoreGraphqlSpi) + api(localLibs.graphql.annotations) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + compileOnly(projects.hypertraceCoreGraphqlAttributeScopeConstants) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.rxjava3) + implementation(commonLibs.hypertrace.gatewayservice.api) + implementation(commonLibs.protobuf.javautil) + + implementation(projects.hypertraceCoreGraphqlContext) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + implementation(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlAttributeStore) + implementation(projects.hypertraceCoreGraphqlDeserialization) + implementation(projects.hypertraceCoreGraphqlSchemaUtils) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.jackson.databind) + testImplementation(projects.hypertraceCoreGraphqlGatewayServiceUtils) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) + + testAnnotationProcessor(commonLibs.lombok) + testCompileOnly(commonLibs.lombok) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/gradle.lockfile new file mode 100644 index 00000000..8621ec44 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/gradle.lockfile @@ -0,0 +1,83 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.8.9=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java-util:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath,testCompileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaFragment.java new file mode 100644 index 00000000..3c9bbfe5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaFragment.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.log.event; + +import org.hypertrace.core.graphql.log.event.schema.LogEventSchema; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class LogEventSchemaFragment implements GraphQlSchemaFragment { + + @Override + public String fragmentName() { + return "LogEvent schema"; + } + + @Override + public Class annotatedQueryClass() { + return LogEventSchema.class; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaModule.java new file mode 100644 index 00000000..25cff0ec --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/LogEventSchemaModule.java @@ -0,0 +1,19 @@ +package org.hypertrace.core.graphql.log.event; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.log.event.dao.LogEventDaoModule; +import org.hypertrace.core.graphql.log.event.request.LogEventRequestModule; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class LogEventSchemaModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), GraphQlSchemaFragment.class) + .addBinding() + .to(LogEventSchemaFragment.class); + + install(new LogEventRequestModule()); + install(new LogEventDaoModule()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventDao.java new file mode 100644 index 00000000..68bd5642 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventDao.java @@ -0,0 +1,68 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.grpc.CallCredentials; +import io.reactivex.rxjava3.core.Single; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.log.event.request.LogEventRequest; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.GatewayServiceGrpc; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; + +@Singleton +class GatewayServiceLogEventDao implements LogEventDao { + private final GatewayServiceFutureStub gatewayServiceStub; + private final GrpcContextBuilder grpcContextBuilder; + private final GatewayServiceLogEventsRequestBuilder requestBuilder; + private final GatewayServiceLogEventsResponseConverter logEventConverter; + private final GraphQlServiceConfig serviceConfig; + + @Inject + GatewayServiceLogEventDao( + GraphQlServiceConfig serviceConfig, + CallCredentials credentials, + GrpcContextBuilder grpcContextBuilder, + GrpcChannelRegistry channelRegistry, + GatewayServiceLogEventsRequestBuilder requestBuilder, + GatewayServiceLogEventsResponseConverter logEventConverter) { + this.grpcContextBuilder = grpcContextBuilder; + this.requestBuilder = requestBuilder; + this.logEventConverter = logEventConverter; + this.serviceConfig = serviceConfig; + + this.gatewayServiceStub = + GatewayServiceGrpc.newFutureStub( + channelRegistry.forAddress( + serviceConfig.getGatewayServiceHost(), serviceConfig.getGatewayServicePort())) + .withCallCredentials(credentials); + } + + @Override + public Single getLogEvents(LogEventRequest request) { + return this.requestBuilder + .buildRequest(request) + .flatMap(serverRequest -> this.makeRequest(request.context(), serverRequest)) + .flatMap(serverResponse -> this.logEventConverter.convert(request, serverResponse)); + } + + private Single makeRequest( + GraphQlRequestContext context, LogEventsRequest request) { + return Single.fromFuture( + this.grpcContextBuilder + .build(context) + .call( + () -> + this.gatewayServiceStub + .withDeadlineAfter( + serviceConfig.getGatewayServiceTimeout().toMillis(), MILLISECONDS) + .getLogEvents(request))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilder.java new file mode 100644 index 00000000..2c051529 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilder.java @@ -0,0 +1,54 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.log.event.request.LogEventRequest; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; + +class GatewayServiceLogEventsRequestBuilder { + + private final Converter>, Filter> filterConverter; + private final Converter>, List> + orderConverter; + private final Converter, Set> attributeConverter; + + @Inject + GatewayServiceLogEventsRequestBuilder( + Converter>, Filter> filterConverter, + Converter>, List> orderConverter, + Converter, Set> attributeConverter) { + this.filterConverter = filterConverter; + this.orderConverter = orderConverter; + this.attributeConverter = attributeConverter; + } + + Single buildRequest(LogEventRequest gqlRequest) { + return zip( + this.attributeConverter.convert(gqlRequest.attributes()), + this.orderConverter.convert(gqlRequest.orderArguments()), + this.filterConverter.convert(gqlRequest.filterArguments()), + (selections, orderBys, filters) -> + LogEventsRequest.newBuilder() + .setStartTimeMillis(gqlRequest.timeRange().startTime().toEpochMilli()) + .setEndTimeMillis(gqlRequest.timeRange().endTime().toEpochMilli()) + .addAllSelection(selections) + .addAllOrderBy(orderBys) + .setLimit(gqlRequest.limit()) + .setOffset(gqlRequest.offset()) + .setFilter(filters) + .build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverter.java new file mode 100644 index 00000000..a5aa2981 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverter.java @@ -0,0 +1,68 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.log.event.request.LogEventRequest; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; + +class GatewayServiceLogEventsResponseConverter { + + private final BiConverter< + Collection, Map, Map> + attributeMapConverter; + + @Inject + GatewayServiceLogEventsResponseConverter( + BiConverter< + Collection, Map, Map> + attributeMapConverter) { + this.attributeMapConverter = attributeMapConverter; + } + + public Single convert(LogEventRequest request, LogEventsResponse response) { + // todo populate total correctly + return Observable.fromIterable(response.getLogEventsList()) + .concatMapSingle(logEvent -> this.convert(request, logEvent)) + .toList() + .map( + logEvents -> + new ConvertedLogEventResultSet(logEvents, logEvents.size(), logEvents.size())); + } + + private Single convert( + LogEventRequest request, org.hypertrace.gateway.service.v1.log.events.LogEvent logEvent) { + return this.attributeMapConverter + .convert(request.attributes(), logEvent.getAttributesMap()) + .map(ConvertedLogEvent::new); + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedLogEvent implements LogEvent { + Map attributeValues; + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedLogEventResultSet implements LogEventResultSet { + List results; + long total; + long count; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDao.java new file mode 100644 index 00000000..de877729 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDao.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.log.event.request.LogEventRequest; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; + +public interface LogEventDao { + + Single getLogEvents(LogEventRequest request); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDaoModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDaoModule.java new file mode 100644 index 00000000..e221a361 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/dao/LogEventDaoModule.java @@ -0,0 +1,59 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.common.Value; + +public class LogEventDaoModule extends AbstractModule { + + @Override + protected void configure() { + bind(LogEventDao.class).to(GatewayServiceLogEventDao.class); + + requireBinding(CallCredentials.class); + requireBinding(GraphQlServiceConfig.class); + requireBinding(GrpcContextBuilder.class); + requireBinding(GrpcChannelRegistry.class); + + requireBinding( + Key.get(new TypeLiteral, Set>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter< + List>, List>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter>, Filter>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + BiConverter< + Collection, + Map, + Map>>() {})); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/fetcher/LogEventFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/fetcher/LogEventFetcher.java new file mode 100644 index 00000000..d6b5a3a5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/fetcher/LogEventFetcher.java @@ -0,0 +1,43 @@ +package org.hypertrace.core.graphql.log.event.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.fetcher.InjectableDataFetcher; +import org.hypertrace.core.graphql.log.event.dao.LogEventDao; +import org.hypertrace.core.graphql.log.event.request.LogEventRequestBuilder; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; + +public class LogEventFetcher extends InjectableDataFetcher { + + public LogEventFetcher() { + super(LogEventFetcherImpl.class); + } + + static final class LogEventFetcherImpl + implements DataFetcher> { + private final LogEventRequestBuilder requestBuilder; + private final LogEventDao logEventDao; + + @Inject + LogEventFetcherImpl(LogEventRequestBuilder requestBuilder, LogEventDao logEventDao) { + this.requestBuilder = requestBuilder; + this.logEventDao = logEventDao; + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + return this.requestBuilder + .build( + contextFromEnvironment(environment), + environment.getArguments(), + environment.getSelectionSet()) + .flatMap(this.logEventDao::getLogEvents) + .toCompletionStage() + .toCompletableFuture(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/DefaultLogEventRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/DefaultLogEventRequestBuilder.java new file mode 100644 index 00000000..afb74e78 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/DefaultLogEventRequestBuilder.java @@ -0,0 +1,140 @@ +package org.hypertrace.core.graphql.log.event.request; + +import static io.reactivex.rxjava3.core.Single.zip; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +public class DefaultLogEventRequestBuilder implements LogEventRequestBuilder { + + private final ArgumentDeserializer argumentDeserializer; + private final GraphQlSelectionFinder selectionFinder; + private final AttributeRequestBuilder attributeRequestBuilder; + private final AttributeAssociator attributeAssociator; + private final FilterRequestBuilder filterRequestBuilder; + private static final int DEFAULT_LIMIT = 100; + private static final int DEFAULT_OFFSET = 0; + + @Inject + DefaultLogEventRequestBuilder( + ArgumentDeserializer argumentDeserializer, + GraphQlSelectionFinder selectionFinder, + AttributeRequestBuilder attributeRequestBuilder, + AttributeAssociator attributeAssociator, + FilterRequestBuilder filterRequestBuilder) { + this.argumentDeserializer = argumentDeserializer; + this.selectionFinder = selectionFinder; + this.attributeRequestBuilder = attributeRequestBuilder; + this.attributeAssociator = attributeAssociator; + this.filterRequestBuilder = filterRequestBuilder; + } + + @Override + public Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet) { + return this.build( + context, + HypertraceCoreAttributeScopeString.LOG_EVENT, + arguments, + selectionSet, + OrderArgument.class); + } + + Single build( + GraphQlRequestContext context, + String requestScope, + Map arguments, + DataFetchingFieldSelectionSet selectionSet, + Class orderArgumentClass) { + int limit = + this.argumentDeserializer + .deserializePrimitive(arguments, LimitArgument.class) + .orElse(DEFAULT_LIMIT); + + int offset = + this.argumentDeserializer + .deserializePrimitive(arguments, OffsetArgument.class) + .orElse(DEFAULT_OFFSET); + + TimeRangeArgument timeRange = + this.argumentDeserializer + .deserializeObject(arguments, TimeRangeArgument.class) + .orElseThrow(); + + List requestedOrders = + this.argumentDeserializer + .deserializeObjectList(arguments, orderArgumentClass) + .orElse(Collections.emptyList()); + + List requestedFilters = + this.argumentDeserializer + .deserializeObjectList(arguments, FilterArgument.class) + .orElse(Collections.emptyList()); + + return zip( + this.attributeRequestBuilder + .buildForAttributeQueryableFields( + context, requestScope, getAttributeQueryableFields(selectionSet)) + .collect(Collectors.toUnmodifiableSet()), + this.attributeAssociator + .associateAttributes( + context, + requestScope, + requestedOrders, + arg -> arg.resolvedKeyExpression().key()) + .collect(Collectors.toUnmodifiableList()), + this.filterRequestBuilder.build(context, requestScope, requestedFilters), + (attributeRequests, orders, filters) -> + Single.just( + new DefaultLogEventRequest( + context, attributeRequests, timeRange, limit, offset, orders, filters))) + .flatMap(single -> single); + } + + private Stream getAttributeQueryableFields( + DataFetchingFieldSelectionSet selectionSet) { + return this.selectionFinder.findSelections( + selectionSet, SelectionQuery.namedChild(ResultSet.RESULT_SET_RESULTS_NAME)); + } + + @Value + @Accessors(fluent = true) + private static class DefaultLogEventRequest implements LogEventRequest { + + GraphQlRequestContext context; + Collection attributes; + TimeRangeArgument timeRange; + int limit; + int offset; + List> orderArguments; + Collection> filterArguments; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequest.java new file mode 100644 index 00000000..23147542 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequest.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.graphql.log.event.request; + +import java.util.Collection; +import java.util.List; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.ContextualRequest; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; + +public interface LogEventRequest extends ContextualRequest { + // All attributes to fetch + Collection attributes(); + + TimeRangeArgument timeRange(); + + int limit(); + + int offset(); + + List> orderArguments(); + + Collection> filterArguments(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestBuilder.java new file mode 100644 index 00000000..93ab55d8 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestBuilder.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.log.event.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.Map; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface LogEventRequestBuilder { + Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestModule.java new file mode 100644 index 00000000..bf8a2b7f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/request/LogEventRequestModule.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.graphql.log.event.request; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; + +public class LogEventRequestModule extends AbstractModule { + + @Override + protected void configure() { + bind(LogEventRequestBuilder.class).to(DefaultLogEventRequestBuilder.class); + requireBinding(ArgumentDeserializer.class); + requireBinding(AttributeRequestBuilder.class); + requireBinding(FilterRequestBuilder.class); + requireBinding(AttributeStore.class); + requireBinding(AttributeAssociator.class); + requireBinding(GraphQlSelectionFinder.class); + requireBinding(AttributeScopeStringTranslator.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEvent.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEvent.java new file mode 100644 index 00000000..75c51b7d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEvent.java @@ -0,0 +1,9 @@ +package org.hypertrace.core.graphql.log.event.schema; + +import graphql.annotations.annotationTypes.GraphQLName; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeQueryable; + +@GraphQLName(LogEvent.TYPE_NAME) +public interface LogEvent extends AttributeQueryable { + String TYPE_NAME = "LogEvent"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventResultSet.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventResultSet.java new file mode 100644 index 00000000..79bddbe0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventResultSet.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.log.event.schema; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; + +@GraphQLName(LogEventResultSet.TYPE_NAME) +public interface LogEventResultSet extends ResultSet { + String TYPE_NAME = "LogEventResultSet"; + + @Override + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULT_SET_RESULTS_NAME) + List results(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventSchema.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventSchema.java new file mode 100644 index 00000000..6a844def --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/main/java/org/hypertrace/core/graphql/log/event/schema/LogEventSchema.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.log.event.schema; + +import graphql.annotations.annotationTypes.GraphQLDataFetcher; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.log.event.fetcher.LogEventFetcher; + +public interface LogEventSchema { + String LOG_EVENTS_QUERY_NAME = "logEvents"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(LOG_EVENTS_QUERY_NAME) + @GraphQLDataFetcher(LogEventFetcher.class) + LogEventResultSet logEvents( + @GraphQLName(TimeRangeArgument.ARGUMENT_NAME) @GraphQLNonNull TimeRangeArgument between, + @GraphQLName(FilterArgument.ARGUMENT_NAME) List filterBy, + @GraphQLName(OrderArgument.ARGUMENT_NAME) List orderBy, + @GraphQLName(LimitArgument.ARGUMENT_NAME) int limit, + @GraphQLName(OffsetArgument.ARGUMENT_NAME) int offset); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/BaseDaoTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/BaseDaoTest.java new file mode 100644 index 00000000..951c0ae4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/BaseDaoTest.java @@ -0,0 +1,69 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.log.event.request.LogEventRequest; + +class BaseDaoTest { + + @Value + @Accessors(fluent = true) + static class DefaultLogEventRequest implements LogEventRequest { + + GraphQlRequestContext context; + Collection attributes; + TimeRangeArgument timeRange; + int limit; + int offset; + List> orderArguments; + Collection> filterArguments; + } + + @Value + @Accessors(fluent = true) + static class DefaultAttributeRequest implements AttributeRequest { + AttributeAssociation attributeExpressionAssociation; + } + + @Value + @Accessors(fluent = true) + static class DefaultAttributeModel implements AttributeModel { + + String id; + String scope; + String key; + String displayName; + AttributeModelType type; + String units; + boolean onlySupportsGrouping; + boolean onlySupportsAggregation; + List supportedMetricAggregationTypes; + boolean groupable; + boolean isCustom; + } + + @Value + @Accessors(fluent = true) + static class DefaultTimeRange implements TimeRangeArgument { + + @JsonProperty(TIME_RANGE_ARGUMENT_START_TIME) + Instant startTime; + + @JsonProperty(TIME_RANGE_ARGUMENT_END_TIME) + Instant endTime; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilderTest.java new file mode 100644 index 00000000..879b57e6 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsRequestBuilderTest.java @@ -0,0 +1,126 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.gateway.GatewayUtilsModule; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GatewayServiceLogEventsRequestBuilderTest extends BaseDaoTest { + + private GatewayServiceLogEventsRequestBuilder requestBuilder; + + @BeforeEach + void setup() { + Injector injector = + Guice.createInjector( + new GatewayUtilsModule(), + new AbstractModule() { + @Override + protected void configure() { + bind(CallCredentials.class).toInstance(mock(CallCredentials.class)); + bind(GraphQlServiceConfig.class).toInstance(mock(GraphQlServiceConfig.class)); + bind(GrpcChannelRegistry.class).toInstance(mock(GrpcChannelRegistry.class)); + } + }); + + Converter>, Filter> filterConverter = + injector.getInstance( + Key.get( + new TypeLiteral< + Converter>, Filter>>() {})); + Converter>, List> orderConverter = + injector.getInstance( + Key.get( + new TypeLiteral< + Converter< + List>, List>>() {})); + Converter, Set> attributeConverter = + injector.getInstance( + Key.get( + new TypeLiteral, Set>>() {})); + + requestBuilder = + new GatewayServiceLogEventsRequestBuilder( + filterConverter, orderConverter, attributeConverter); + } + + @Test + void testBuildRequest() { + long startTime = System.currentTimeMillis(); + long endTime = System.currentTimeMillis() + Duration.ofHours(1).toMillis(); + Collection attributeRequests = + List.of( + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "traceId", + "LOG_EVENT", + "traceId", + "Trace Id", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("traceId"))), + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "timestamp", + "LOG_EVENT", + "timestamp", + "Timestamp", + AttributeModelType.TIMESTAMP, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("timestamp")))); + DefaultLogEventRequest defaultLogEventRequest = + new DefaultLogEventRequest( + null, + attributeRequests, + new DefaultTimeRange(Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)), + 0, + 0, + List.of(), + Collections.emptyList()); + LogEventsRequest logEventRequest = + requestBuilder.buildRequest(defaultLogEventRequest).blockingGet(); + + assertEquals(endTime, logEventRequest.getEndTimeMillis()); + assertEquals(startTime, logEventRequest.getStartTimeMillis()); + assertEquals(2, logEventRequest.getSelectionCount()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverterTest.java new file mode 100644 index 00000000..4d472936 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-log-event-schema/src/test/java/org/hypertrace/core/graphql/log/event/dao/GatewayServiceLogEventsResponseConverterTest.java @@ -0,0 +1,141 @@ +package org.hypertrace.core.graphql.log.event.dao; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.gateway.GatewayUtilsModule; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.hypertrace.gateway.service.v1.log.events.LogEvent; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GatewayServiceLogEventsResponseConverterTest extends BaseDaoTest { + + private GatewayServiceLogEventsResponseConverter responseConverter; + + @BeforeEach + void setup() { + Injector injector = + Guice.createInjector( + new GatewayUtilsModule(), + new AbstractModule() { + @Override + protected void configure() { + bind(CallCredentials.class).toInstance(mock(CallCredentials.class)); + bind(GraphQlServiceConfig.class).toInstance(mock(GraphQlServiceConfig.class)); + bind(GrpcChannelRegistry.class).toInstance(mock(GrpcChannelRegistry.class)); + } + }); + BiConverter, Map, Map> + attributeMapConverter = + injector.getInstance( + Key.get( + new TypeLiteral< + BiConverter< + Collection, + Map, + Map>>() {})); + responseConverter = new GatewayServiceLogEventsResponseConverter(attributeMapConverter); + } + + @Test + void testConvert() { + long startTime = System.currentTimeMillis(); + long endTime = System.currentTimeMillis() + Duration.ofHours(1).toMillis(); + LogEventsResponse logEventsResponse = + LogEventsResponse.newBuilder() + .addLogEvents( + LogEvent.newBuilder() + .putAllAttributes( + Map.of( + "traceId", + Value.newBuilder() + .setString("trace1") + .setValueType(ValueType.STRING) + .build(), + "timestamp", + Value.newBuilder() + .setValueType(ValueType.TIMESTAMP) + .setTimestamp(Duration.ofMillis(startTime).toNanos()) + .build()))) + .build(); + Collection attributeRequests = + List.of( + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "traceId", + "LOG_EVENT", + "traceId", + "Trace Id", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("traceId"))), + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "timestamp", + "LOG_EVENT", + "timestamp", + "Timestamp", + AttributeModelType.TIMESTAMP, + "ns", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("timestamp")))); + DefaultLogEventRequest defaultLogEventRequest = + new DefaultLogEventRequest( + null, + attributeRequests, + new DefaultTimeRange(Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)), + 0, + 0, + List.of(), + Collections.emptyList()); + LogEventResultSet logEventResultSet = + responseConverter.convert(defaultLogEventRequest, logEventsResponse).blockingGet(); + assertEquals(1, logEventResultSet.results().size()); + assertEquals( + "trace1", + logEventResultSet + .results() + .get(0) + .attribute(AttributeExpression.forAttributeKey("traceId"))); + assertEquals( + Instant.ofEpochSecond(0, Duration.ofMillis(startTime).toNanos()), + logEventResultSet + .results() + .get(0) + .attribute(AttributeExpression.forAttributeKey("timestamp"))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/build.gradle.kts new file mode 100644 index 00000000..cf412bf9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(projects.hypertraceCoreGraphqlSpi) + api(localLibs.graphql.annotations) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.rxjava3) + implementation(projects.hypertraceCoreGraphqlContext) + implementation(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlAttributeStore) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/gradle.lockfile new file mode 100644 index 00000000..62f4d26e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/gradle.lockfile @@ -0,0 +1,81 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=runtimeClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=runtimeClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaFragment.java new file mode 100644 index 00000000..5be4c86c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaFragment.java @@ -0,0 +1,35 @@ +package org.hypertrace.core.graphql.metadata; + +import graphql.annotations.processor.typeFunctions.TypeFunction; +import java.util.List; +import javax.annotation.Nonnull; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.schema.typefunctions.AttributeScopeDynamicEnum; +import org.hypertrace.core.graphql.metadata.schema.MetadataSchema; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class MetadataSchemaFragment implements GraphQlSchemaFragment { + + private final TypeFunction attributeScopeDynamicEnum; + + @Inject + MetadataSchemaFragment(AttributeScopeDynamicEnum attributeScopeDynamicEnum) { + this.attributeScopeDynamicEnum = attributeScopeDynamicEnum; + } + + @Override + public String fragmentName() { + return "Metadata schema"; + } + + @Override + public Class annotatedQueryClass() { + return MetadataSchema.class; + } + + @Nonnull + @Override + public List typeFunctions() { + return List.of(this.attributeScopeDynamicEnum); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaModule.java new file mode 100644 index 00000000..49ece31b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/MetadataSchemaModule.java @@ -0,0 +1,33 @@ +package org.hypertrace.core.graphql.metadata; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class MetadataSchemaModule extends AbstractModule { + + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), GraphQlSchemaFragment.class) + .addBinding() + .to(MetadataSchemaFragment.class); + + requireBinding(AttributeStore.class); + + requireBinding(Key.get(new TypeLiteral>() {})); + requireBinding( + Key.get( + new TypeLiteral< + Converter>() {})); + requireBinding(AttributeScopeStringTranslator.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcher.java new file mode 100644 index 00000000..260f72ab --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcher.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.metadata.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.fetcher.InjectableDataFetcher; +import org.hypertrace.core.graphql.metadata.response.MetadataResponseBuilder; +import org.hypertrace.core.graphql.metadata.schema.AttributeMetadata; + +public class MetadataFetcher extends InjectableDataFetcher> { + + public MetadataFetcher() { + super(MetadataFetcherImpl.class); + } + + static final class MetadataFetcherImpl + implements DataFetcher>> { + private final MetadataResponseBuilder responseBuilder; + private final AttributeStore attributeStore; + + @Inject + MetadataFetcherImpl(MetadataResponseBuilder responseBuilder, AttributeStore attributeStore) { + this.responseBuilder = responseBuilder; + this.attributeStore = attributeStore; + } + + @Override + public CompletableFuture> get(DataFetchingEnvironment environment) { + return this.attributeStore + .getAllExternal(contextFromEnvironment(environment)) + .flatMap(this.responseBuilder::build) + .toCompletionStage() + .toCompletableFuture(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/DefaultAttributeMetadata.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/DefaultAttributeMetadata.java new file mode 100644 index 00000000..d0cbb15f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/DefaultAttributeMetadata.java @@ -0,0 +1,29 @@ +package org.hypertrace.core.graphql.metadata.response; + +import static lombok.AccessLevel.PRIVATE; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.metadata.schema.AttributeMetadata; + +@Value +@Builder +@Accessors(fluent = true) +@AllArgsConstructor(access = PRIVATE) +class DefaultAttributeMetadata implements AttributeMetadata { + String scope; + String name; + String displayName; + AttributeType type; + String units; + boolean onlySupportsGrouping; + boolean onlySupportsAggregation; + List supportedAggregations; + boolean groupable; + boolean isCustom; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilder.java new file mode 100644 index 00000000..d0a0f151 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilder.java @@ -0,0 +1,73 @@ +package org.hypertrace.core.graphql.metadata.response; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.metadata.schema.AttributeMetadata; + +public class MetadataResponseBuilder { + + private final Converter typeConverter; + private final Converter + aggregationTypeConverter; + private final AttributeScopeStringTranslator scopeStringTranslator; + + @Inject + MetadataResponseBuilder( + Converter typeConverter, + Converter + aggregationTypeConverter, + AttributeScopeStringTranslator scopeStringTranslator) { + this.typeConverter = typeConverter; + this.aggregationTypeConverter = aggregationTypeConverter; + this.scopeStringTranslator = scopeStringTranslator; + } + + public Single> build(List modelList) { + return Observable.fromIterable(modelList) + .flatMapMaybe(this::build) + .collect(Collectors.toUnmodifiableList()); + } + + public Maybe build(AttributeModel model) { + return zip( + this.convertMetricAggregationTypes(model.supportedMetricAggregationTypes()), + this.typeConverter.convert(model.type()), + (aggregations, type) -> + DefaultAttributeMetadata.builder() + .scope(this.scopeStringTranslator.toExternal(model.scope())) + .name(model.key()) + .displayName(model.displayName()) + .type(type) + .units(model.units()) + .onlySupportsGrouping(model.onlySupportsGrouping()) + .onlySupportsAggregation(model.onlySupportsAggregation()) + .supportedAggregations(aggregations) + .groupable(model.groupable()) + .isCustom(model.isCustom()) + .build()) + .cast(AttributeMetadata.class) + .onErrorComplete(); + } + + private Single> convertMetricAggregationTypes( + List aggregationTypes) { + return Observable.fromIterable(aggregationTypes) + .flatMapMaybe( + aggregationType -> + this.aggregationTypeConverter.convert(aggregationType).onErrorComplete()) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/AttributeMetadata.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/AttributeMetadata.java new file mode 100644 index 00000000..f3bc97fa --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/AttributeMetadata.java @@ -0,0 +1,76 @@ +package org.hypertrace.core.graphql.metadata.schema; + +import graphql.annotations.annotationTypes.GraphQLDescription; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; + +@GraphQLName(AttributeMetadata.TYPE_NAME) +public interface AttributeMetadata { + String TYPE_NAME = "AttributeMetadata"; + String ATTRIBUTE_METADATA_SCOPE_NAME = "scope"; + String ATTRIBUTE_METADATA_NAME_NAME = "name"; + String ATTRIBUTE_METADATA_DISPLAY_NAME = "displayName"; + String ATTRIBUTE_METADATA_TYPE_NAME = "type"; + String ATTRIBUTE_METADATA_UNITS_NAME = "units"; + String ATTRIBUTE_METADATA_ONLY_SUPPORTS_AGGREGATION_NAME = "onlySupportsAggregation"; + String ATTRIBUTE_METADATA_ONLY_SUPPORTS_GROUPING_NAME = "onlySupportsGrouping"; + String ATTRIBUTE_METADATA_SUPPORTED_AGGREGATIONS_NAME = "supportedAggregations"; + String ATTRIBUTE_METADATA_GROUPABLE_NAME = "groupable"; + String ATTRIBUTE_METADATA_IS_CUSTOM = "isCustom"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_SCOPE_NAME) + String scope(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_NAME_NAME) + String name(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_DISPLAY_NAME) + String displayName(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_TYPE_NAME) + AttributeType type(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_UNITS_NAME) + String units(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_ONLY_SUPPORTS_GROUPING_NAME) + @GraphQLDescription("Signifies an attribute is only available in a grouped query") + boolean onlySupportsGrouping(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_ONLY_SUPPORTS_AGGREGATION_NAME) + @GraphQLDescription("Signifies an attribute is only available as an aggregation on all queries") + boolean onlySupportsAggregation(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_SUPPORTED_AGGREGATIONS_NAME) + List supportedAggregations(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_GROUPABLE_NAME) + boolean groupable(); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(ATTRIBUTE_METADATA_IS_CUSTOM) + boolean isCustom(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/MetadataSchema.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/MetadataSchema.java new file mode 100644 index 00000000..6c23aff4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/main/java/org/hypertrace/core/graphql/metadata/schema/MetadataSchema.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.metadata.schema; + +import graphql.annotations.annotationTypes.GraphQLDataFetcher; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.metadata.fetcher.MetadataFetcher; + +public interface MetadataSchema { + String METADATA_SCHEMA_METADATA_NAME = "metadata"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(METADATA_SCHEMA_METADATA_NAME) + @GraphQLDataFetcher(MetadataFetcher.class) + List metadata(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcherTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcherTest.java new file mode 100644 index 00000000..effe854d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/fetcher/MetadataFetcherTest.java @@ -0,0 +1,56 @@ +package org.hypertrace.core.graphql.metadata.fetcher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetchingEnvironment; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.metadata.fetcher.MetadataFetcher.MetadataFetcherImpl; +import org.hypertrace.core.graphql.metadata.response.MetadataResponseBuilder; +import org.hypertrace.core.graphql.metadata.schema.AttributeMetadata; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MetadataFetcherTest { + + @Mock MetadataResponseBuilder mockResponseBuilder; + @Mock AttributeStore mockAttributeStore; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + DataFetchingEnvironment mockDataFetchingEnvironment; + + @Mock AttributeModel mockModel; + @Mock AttributeMetadata mockMetadata; + @Mock GraphQlRequestContext mockContext; + private MetadataFetcherImpl fetcher; + + @BeforeEach + void beforeEach() { + List mockModelResult = List.of(mockModel); + List mockMetadataResult = List.of(mockMetadata); + when(this.mockDataFetchingEnvironment.getGraphQlContext().get(GraphQlRequestContext.class)) + .thenReturn(this.mockContext); + when(this.mockAttributeStore.getAllExternal(eq(this.mockContext))) + .thenReturn(Single.just(mockModelResult)); + when(this.mockResponseBuilder.build(eq(mockModelResult))) + .thenReturn(Single.just(mockMetadataResult)); + + this.fetcher = new MetadataFetcherImpl(this.mockResponseBuilder, this.mockAttributeStore); + } + + @Test + void returnsResponseFuture() throws ExecutionException, InterruptedException { + assertEquals(List.of(mockMetadata), this.fetcher.get(this.mockDataFetchingEnvironment).get()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilderTest.java new file mode 100644 index 00000000..b56522d6 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-metadata-schema/src/test/java/org/hypertrace/core/graphql/metadata/response/MetadataResponseBuilderTest.java @@ -0,0 +1,112 @@ +package org.hypertrace.core.graphql.metadata.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeType; +import org.hypertrace.core.graphql.common.schema.attributes.MetricAggregationType; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MetadataResponseBuilderTest { + + private MetadataResponseBuilder builder; + private List models; + @Mock private Converter mockTypeConverter; + + @Mock + private Converter + mockAggregationTypeConverter; + + @Mock private AttributeScopeStringTranslator mockScopeTranslator; + + @BeforeEach + void beforeEach() { + this.builder = + new MetadataResponseBuilder( + this.mockTypeConverter, this.mockAggregationTypeConverter, this.mockScopeTranslator); + AttributeModel mockModel = mock(AttributeModel.class); + when(mockModel.scope()).thenReturn("TRACE"); + when(mockModel.key()).thenReturn("key"); + when(mockModel.displayName()).thenReturn("display name"); + when(mockModel.type()).thenReturn(AttributeModelType.STRING); + when(mockModel.units()).thenReturn("unit"); + when(mockModel.groupable()).thenReturn(true); + when(mockModel.isCustom()).thenReturn(true); + when(mockModel.onlySupportsGrouping()).thenReturn(false); + when(mockModel.onlySupportsAggregation()).thenReturn(true); + when(mockModel.supportedMetricAggregationTypes()) + .thenReturn( + List.of( + AttributeModelMetricAggregationType.SUM, AttributeModelMetricAggregationType.AVG)); + when(this.mockTypeConverter.convert(eq(AttributeModelType.STRING))) + .thenReturn(Single.just(AttributeType.STRING)); + when(this.mockAggregationTypeConverter.convert(eq(AttributeModelMetricAggregationType.SUM))) + .thenReturn(Single.just(MetricAggregationType.SUM)); + when(this.mockAggregationTypeConverter.convert(eq(AttributeModelMetricAggregationType.AVG))) + .thenReturn(Single.just(MetricAggregationType.AVG)); + when(this.mockScopeTranslator.toExternal("TRACE")).thenReturn("TRACE_EXTERNAL"); + this.models = List.of(mockModel); + } + + @Test + void canBuildResponse() { + assertEquals( + List.of( + DefaultAttributeMetadata.builder() + .scope("TRACE_EXTERNAL") + .name("key") + .displayName("display name") + .type(AttributeType.STRING) + .units("unit") + .onlySupportsGrouping(false) + .onlySupportsAggregation(true) + .supportedAggregations( + List.of(MetricAggregationType.SUM, MetricAggregationType.AVG)) + .groupable(true) + .isCustom(true) + .build()), + this.builder.build(this.models).blockingGet()); + } + + @Test + @SuppressWarnings("unchecked") + void filtersAnyAggregationConversionErrors() { + reset(this.mockAggregationTypeConverter); + when(this.mockAggregationTypeConverter.convert(eq(AttributeModelMetricAggregationType.AVG))) + .thenReturn(Single.just(MetricAggregationType.AVG)); + + when(this.mockAggregationTypeConverter.convert(eq(AttributeModelMetricAggregationType.SUM))) + .thenReturn(Single.error(new RuntimeException())); + + assertEquals( + List.of( + DefaultAttributeMetadata.builder() + .scope("TRACE_EXTERNAL") + .name("key") + .displayName("display name") + .type(AttributeType.STRING) + .units("unit") + .onlySupportsGrouping(false) + .onlySupportsAggregation(true) + .supportedAggregations(List.of(MetricAggregationType.AVG)) + .groupable(true) + .isCustom(true) + .build()), + this.builder.build(this.models).blockingGet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/build.gradle.kts new file mode 100644 index 00000000..aa9d0684 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(projects.hypertraceCoreGraphqlCommonSchema) + + testAnnotationProcessor(commonLibs.lombok) + testCompileOnly(commonLibs.lombok) + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/gradle.lockfile new file mode 100644 index 00000000..21625be4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/gradle.lockfile @@ -0,0 +1,75 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=runtimeClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.protobuf:protobuf-java:3.24.1=runtimeClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.0=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.0=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=testCompileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformation.java b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformation.java new file mode 100644 index 00000000..dd8ae665 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformation.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.graphql.request.transformation; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.common.request.ContextualRequest; + +public interface RequestTransformation { + + /** + * Any request may be provided to this method, which should return true if this transformation + * supports the request and false otherwise. + */ + boolean supportsRequest(ContextualRequest request); + + /** + * Applies a transformation to the request. This will only receive requests that have previously + * returned true from {@link #supportsRequest(ContextualRequest)}, and must always return a + * request, potentially the same request passed in if no transformation is necessary. + */ + Single transform(T request); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformationModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformationModule.java new file mode 100644 index 00000000..30b5ea88 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformationModule.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.request.transformation; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; + +public class RequestTransformationModule extends AbstractModule { + + @Override + protected void configure() { + bind(RequestTransformer.class).to(RequestTransformerImpl.class); + Multibinder.newSetBinder(binder(), RequestTransformation.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformer.java b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformer.java new file mode 100644 index 00000000..63ca96b2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformer.java @@ -0,0 +1,12 @@ +package org.hypertrace.core.graphql.request.transformation; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.common.request.ContextualRequest; + +/** + * A request transformer that can receive any request and will apply all applicable transformations + * that have been registered (currently, the order is undefined), returning the result. + */ +public interface RequestTransformer { + Single transform(T request); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImpl.java b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImpl.java new file mode 100644 index 00000000..d3fba531 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/main/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImpl.java @@ -0,0 +1,27 @@ +package org.hypertrace.core.graphql.request.transformation; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.ContextualRequest; + +class RequestTransformerImpl implements RequestTransformer { + private final Set requestTransformations; + + @Inject + RequestTransformerImpl(Set requestTransformations) { + this.requestTransformations = requestTransformations; + } + + @Override + public Single transform(T request) { + return Observable.fromIterable(requestTransformations) + .filter(requestTransformation -> requestTransformation.supportsRequest(request)) + .reduce( + Single.just(request), // Work in singles since there's no flat reduce, then unwrap later + (currentRequestSingle, requestTransformation) -> + currentRequestSingle.flatMap(requestTransformation::transform)) + .flatMap(wrapped -> wrapped); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/test/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImplTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/test/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImplTest.java new file mode 100644 index 00000000..62ddd09b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-request-transformation/src/test/java/org/hypertrace/core/graphql/request/transformation/RequestTransformerImplTest.java @@ -0,0 +1,85 @@ +package org.hypertrace.core.graphql.request.transformation; + +import static org.mockito.Mockito.mock; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.observers.TestObserver; +import java.util.Set; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.ContextualRequest; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.junit.jupiter.api.Test; + +class RequestTransformerImplTest { + + @Test + void noApplicableTransformerReturnsInput() { + RequestTransformer transformer = new RequestTransformerImpl(Set.of()); + + ContextualRequest request = mock(ContextualRequest.class); + + TestObserver testObserver = new TestObserver<>(); + transformer.transform(request).subscribe(testObserver); + + testObserver.assertResult(request); + } + + @Test + void skipsUnsupportedTransformers() { + GraphQlRequestContext context = mock(GraphQlRequestContext.class); + RequestTransformer transformer = + new RequestTransformerImpl( + Set.of( + new TestTransformation(true, "prefix-", ""), + new TestTransformation(false, "", "-suffix"))); + + TestObserver testObserver = new TestObserver<>(); + transformer.transform(new TestContextualRequest(context, "original")).subscribe(testObserver); + + testObserver.assertResult(new TestContextualRequest(context, "prefix-original")); + } + + @Test + void appliesAllTransformers() { + GraphQlRequestContext context = mock(GraphQlRequestContext.class); + RequestTransformer transformer = + new RequestTransformerImpl( + Set.of( + new TestTransformation(true, "prefix-", ""), + new TestTransformation(true, "", "-suffix"))); + + TestObserver testObserver = new TestObserver<>(); + transformer.transform(new TestContextualRequest(context, "original")).subscribe(testObserver); + + testObserver.assertResult(new TestContextualRequest(context, "prefix-original-suffix")); + } + + @Value + private static class TestTransformation implements RequestTransformation { + boolean supported; + String prefixToAdd; + String suffixToAdd; + + @Override + public boolean supportsRequest(ContextualRequest request) { + return supported; + } + + @Override + @SuppressWarnings("unchecked") + public Single transform(T request) { + String previousValue = ((TestContextualRequest) request).value(); + TestContextualRequest newRequest = + new TestContextualRequest(request.context(), prefixToAdd + previousValue + suffixToAdd); + return Single.just((T) newRequest); + } + } + + @Value + @Accessors(fluent = true) + private static class TestContextualRequest implements ContextualRequest { + GraphQlRequestContext context; + String value; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/build.gradle.kts new file mode 100644 index 00000000..39ebc699 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.rxjava3) + api(commonLibs.guice) + implementation(projects.hypertraceCoreGraphqlSpi) + implementation(commonLibs.guava) +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/gradle.lockfile new file mode 100644 index 00000000..4be40ba5 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/gradle.lockfile @@ -0,0 +1,28 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoScheduler.java b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoScheduler.java new file mode 100644 index 00000000..f3cbcaf4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoScheduler.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.rx; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +@Qualifier +@Target({FIELD, PARAMETER, METHOD}) +@Retention(RUNTIME) +public @interface BoundedIoScheduler {} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoSchedulerProvider.java b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoSchedulerProvider.java new file mode 100644 index 00000000..3d3ece54 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/BoundedIoSchedulerProvider.java @@ -0,0 +1,46 @@ +package org.hypertrace.core.graphql.rx; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.schedulers.Schedulers; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import javax.inject.Inject; +import javax.inject.Provider; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; + +/** + * A scheduler using up to a configuration-based number of threads. + * + *

This differs from the default Scheduler.io implementation which uses an unbounded number of + * threads. + */ +class BoundedIoSchedulerProvider implements Provider { + + private final Scheduler scheduler; + private final GraphQlEndpointConfig endpointConfig; + + @Inject + BoundedIoSchedulerProvider(GraphQlEndpointConfig endpointConfig) { + this.endpointConfig = endpointConfig; + this.scheduler = Schedulers.from(this.buildExecutor()); + } + + @Override + public Scheduler get() { + return this.scheduler; + } + + private Executor buildExecutor() { + return Executors.newFixedThreadPool( + this.endpointConfig.getMaxIoThreads(), this.buildThreadFactory()); + } + + private ThreadFactory buildThreadFactory() { + return new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("rx-bounded-io-scheduler-%d") + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/RxUtilModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/RxUtilModule.java new file mode 100644 index 00000000..527443e9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-rx-utils/src/main/java/org/hypertrace/core/graphql/rx/RxUtilModule.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.rx; + +import com.google.inject.AbstractModule; +import io.reactivex.rxjava3.core.Scheduler; +import javax.inject.Singleton; + +public class RxUtilModule extends AbstractModule { + @Override + protected void configure() { + bind(Scheduler.class) + .annotatedWith(BoundedIoScheduler.class) + .toProvider(BoundedIoSchedulerProvider.class) + .in(Singleton.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/build.gradle.kts new file mode 100644 index 00000000..3350af49 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.graphql.java) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.guice) + + implementation(projects.hypertraceCoreGraphqlSpi) + implementation(localLibs.graphql.annotations) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/gradle.lockfile new file mode 100644 index 00000000..50f0b4b8 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/gradle.lockfile @@ -0,0 +1,46 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultGraphQlSchemaRegistry.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultGraphQlSchemaRegistry.java new file mode 100644 index 00000000..0b31cb17 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultGraphQlSchemaRegistry.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.graphql.schema.registry; + +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +class DefaultGraphQlSchemaRegistry implements GraphQlSchemaRegistry { + + private final Set schemaFragments; + private final DefaultSchema defaultSchemaFragment; + + @Inject + DefaultGraphQlSchemaRegistry( + Set schemaFragments, DefaultSchema rootSchemaFragment) { + this.schemaFragments = Set.copyOf(schemaFragments); + this.defaultSchemaFragment = rootSchemaFragment; + } + + @Override + public Set getRegisteredFragments() { + return this.schemaFragments; + } + + @Override + public DefaultSchema getRootFragment() { + return this.defaultSchemaFragment; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultSchema.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultSchema.java new file mode 100644 index 00000000..4f6ecf4e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/DefaultSchema.java @@ -0,0 +1,44 @@ +package org.hypertrace.core.graphql.schema.registry; + +import graphql.annotations.annotationTypes.GraphQLDescription; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +class DefaultSchema implements GraphQlSchemaFragment { + // Placeholder description is used to identify and remove placeholder fields before building the + // schema. Placeholders are used while the schema is under construction so it is always valid + // (i.e. has at least one value) + static final String PLACEHOLDER_DESCRIPTION = "::placeholder::"; + static final String ROOT_QUERY_NAME = "Query"; + static final String ROOT_MUTATION_NAME = "Mutation"; + + @Override + public String fragmentName() { + return "Default Schema"; + } + + @Override + public Class annotatedQueryClass() { + return QuerySchema.class; + } + + @Override + public Class annotatedMutationClass() { + return MutationSchema.class; + } + + @GraphQLName(ROOT_QUERY_NAME) + private interface QuerySchema { + @GraphQLField + @GraphQLDescription(PLACEHOLDER_DESCRIPTION) + String placeholder(); + } + + @GraphQLName(ROOT_MUTATION_NAME) + private interface MutationSchema { + @GraphQLField + @GraphQLDescription(PLACEHOLDER_DESCRIPTION) + String placeholder(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMerger.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMerger.java new file mode 100644 index 00000000..45aaf2aa --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMerger.java @@ -0,0 +1,190 @@ +package org.hypertrace.core.graphql.schema.registry; + +import static java.util.Objects.nonNull; + +import graphql.annotations.AnnotationsSchemaCreator; +import graphql.annotations.processor.GraphQLAnnotations; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLCodeRegistry.Builder; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Provider; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +class GraphQlAnnotatedSchemaMerger implements Provider { + + private final GraphQlSchemaRegistry registry; + private final GraphQlEndpointConfig config; + + @Inject + GraphQlAnnotatedSchemaMerger(GraphQlSchemaRegistry registry, GraphQlEndpointConfig config) { + this.registry = registry; + this.config = config; + } + + @Override + public GraphQLSchema get() { + GraphQlSchemaFragment rootFragment = this.registry.getRootFragment(); + Set fragments = this.registry.getRegisteredFragments(); + GraphQLAnnotations annotationProcessor = new GraphQLAnnotations(); + + this.registerAllTypeFunctions(fragments, annotationProcessor); + + return fragments.stream() + .map(fragment -> this.fragmentToSchema(fragment, annotationProcessor)) + .reduce(this.fragmentToSchema(rootFragment, annotationProcessor), this::merge) + .transform(this::removeRootPlaceholders) + .transform(this::applyConfig); + } + + private void registerAllTypeFunctions( + Collection fragments, GraphQLAnnotations annotationProcessor) { + fragments.stream() + .map(GraphQlSchemaFragment::typeFunctions) + .flatMap(Collection::stream) + .forEach(annotationProcessor::registerTypeFunction); + } + + private GraphQLSchema fragmentToSchema( + GraphQlSchemaFragment fragment, GraphQLAnnotations annotationProcessor) { + AnnotationsSchemaCreator.Builder builder = + AnnotationsSchemaCreator.newAnnotationsSchema() + .setAnnotationsProcessor(annotationProcessor) + .mutation(fragment.annotatedMutationClass()); + + // Query must be assigned, use our root query object as a placeholder if our fragment defines a + // mutation only + if (nonNull(fragment.annotatedQueryClass())) { + builder.query(fragment.annotatedQueryClass()); + } else { + builder.query(this.registry.getRootFragment().annotatedQueryClass()); + } + + return builder.build(); + } + + private GraphQLSchema merge(GraphQLSchema accumulatedSchema, GraphQLSchema schemaToAdd) { + return GraphQLSchema.newSchema() + .query(this.mergeObjects(accumulatedSchema.getQueryType(), schemaToAdd.getQueryType())) + .mutation( + this.mergeObjects(accumulatedSchema.getMutationType(), schemaToAdd.getMutationType())) + .codeRegistry( + this.mergeCodeRegistries( + this.buildMapWithoutNulls( + Set.of( + new SimpleImmutableEntry<>( + schemaToAdd.getQueryType(), accumulatedSchema.getQueryType()), + new SimpleImmutableEntry<>( + schemaToAdd.getMutationType(), accumulatedSchema.getMutationType()))), + accumulatedSchema.getCodeRegistry(), + schemaToAdd.getCodeRegistry())) + .build(); + } + + @Nullable + private GraphQLObjectType mergeObjects( + @NonNull GraphQLObjectType accumulatedObject, @Nullable GraphQLObjectType objectToAdd) { + + if (objectToAdd == null) { + return accumulatedObject; + } + + return GraphQLObjectType.newObject(accumulatedObject) + .fields(objectToAdd.getFieldDefinitions()) + .build(); + } + + /* + The remapping of objects accounts for the fact that data fetchers are scoped to their parent + object name. When we merge objects, their name can change and we need to put in a new pointer to + the data fetcher for that new name. + */ + private GraphQLCodeRegistry mergeCodeRegistries( + Map remappedObjects, + GraphQLCodeRegistry accumulatingRegistry, + GraphQLCodeRegistry registryToMerge) { + + Builder updatedRegistryBuilder = + GraphQLCodeRegistry.newCodeRegistry(accumulatingRegistry) + .typeResolvers(registryToMerge) + .dataFetchers(registryToMerge); + remappedObjects.forEach( + (objectToMerge, accumulatingObject) -> + objectToMerge + .getFieldDefinitions() + .forEach( + graphQLFieldDefinition -> + updatedRegistryBuilder.dataFetcher( + accumulatingObject, + graphQLFieldDefinition, + registryToMerge.getDataFetcher( + objectToMerge, graphQLFieldDefinition)))); + + return updatedRegistryBuilder.build(); + } + + private Map buildMapWithoutNulls( + Collection> entries) { + return entries.stream() + .filter(entry -> nonNull(entry) && nonNull(entry.getKey()) && nonNull(entry.getValue())) + .collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue)); + } + + private void removeRootPlaceholders(GraphQLSchema.Builder mergedSchema) { + GraphQLSchema mergedWithPlaceholders = mergedSchema.build(); + // Queries must be defined, mutation is optional which is represented by a null + mergedSchema.query( + this.rebuildTypeWithoutPlaceholders(mergedWithPlaceholders.getQueryType()).orElseThrow()); + mergedSchema.mutation( + this.rebuildTypeWithoutPlaceholders(mergedWithPlaceholders.getMutationType()).orElse(null)); + } + + private void applyConfig(GraphQLSchema.Builder builder) { + if (!this.config.isIntrospectionAllowed()) { + GraphQLSchema schemaSoFar = builder.build(); + GraphQLCodeRegistry registryWithoutIntrospection = + schemaSoFar + .getCodeRegistry() + .transform( + registry -> + registry.fieldVisibility( + NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)); + builder.codeRegistry(registryWithoutIntrospection); + } + } + + private Optional rebuildTypeWithoutPlaceholders(GraphQLObjectType objectType) { + // Schema validation now requires all objects to have at least one field. To merge partial + // fragments, we start with placeholders, identified by a constant, then remove them at the end. + GraphQLObjectType newType = + GraphQLObjectType.newObject(objectType) + .replaceFields( + objectType.getFields().stream() + .filter( + field -> + Optional.ofNullable(field.getDescription()) + .map( + description -> + !description.equals(DefaultSchema.PLACEHOLDER_DESCRIPTION)) + .orElse(true)) + .collect(Collectors.toList())) + .build(); + + if (newType.getFields().isEmpty()) { + return Optional.empty(); + } + return Optional.of(newType); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistry.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistry.java new file mode 100644 index 00000000..416bdd76 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistry.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.schema.registry; + +import java.util.Set; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public interface GraphQlSchemaRegistry { + + Set getRegisteredFragments(); + + GraphQlSchemaFragment getRootFragment(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistryModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistryModule.java new file mode 100644 index 00000000..5ed3c707 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/main/java/org/hypertrace/core/graphql/schema/registry/GraphQlSchemaRegistryModule.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.schema.registry; + +import com.google.inject.AbstractModule; +import graphql.schema.GraphQLSchema; + +public class GraphQlSchemaRegistryModule extends AbstractModule { + + @Override + protected void configure() { + bind(GraphQlSchemaRegistry.class).to(DefaultGraphQlSchemaRegistry.class); + bind(GraphQLSchema.class).toProvider(GraphQlAnnotatedSchemaMerger.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/test/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMergerTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/test/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMergerTest.java new file mode 100644 index 00000000..21e4cff9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-registry/src/test/java/org/hypertrace/core/graphql/schema/registry/GraphQlAnnotatedSchemaMergerTest.java @@ -0,0 +1,278 @@ +package org.hypertrace.core.graphql.schema.registry; + +import static org.hypertrace.core.graphql.schema.registry.DefaultSchema.ROOT_MUTATION_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import graphql.Scalars; +import graphql.annotations.annotationTypes.GraphQLDataFetcher; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.processor.ProcessingElementsContainer; +import graphql.annotations.processor.typeFunctions.TypeFunction; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility; +import java.lang.reflect.AnnotatedType; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class GraphQlAnnotatedSchemaMergerTest { + + @Mock private GraphQlSchemaRegistry mockRegistry; + @Mock private DataFetchingEnvironment mockDataFetchingEnvironment; + @Mock private GraphQlEndpointConfig mockConfig; + + private GraphQlAnnotatedSchemaMerger merger; + + interface FirstQuerySchema { + @GraphQLField + String first(); + + @GraphQLField + SharedType second(); + } + + interface SecondQuerySchema { + @GraphQLField + SharedType third(); + } + + interface ThirdQuerySchema { + @GraphQLDataFetcher(CustomDataFetcher.class) + @GraphQLField + String fourth(); + } + + interface FirstMutationSchema { + @GraphQLField + SharedType mutateOne(String argument); + } + + interface SecondMutationSchema { + @GraphQLField + SharedType mutateTwo(String argument); + } + + public static class CustomDataFetcher implements DataFetcher { + + @Override + public String get(DataFetchingEnvironment environment) { + return "custom"; + } + } + + interface SharedType { + @GraphQLField + @GraphQLDataFetcher(CustomDataFetcher.class) + String sharedValue(); + } + + @BeforeEach + public void beforeEach() { + this.merger = new GraphQlAnnotatedSchemaMerger(mockRegistry, mockConfig); + + when(mockRegistry.getRootFragment()).thenReturn(new DefaultSchema()); + } + + @Test + void mergesSingleSchema() throws Exception { + when(mockRegistry.getRegisteredFragments()) + .thenReturn(Set.of(this.createSchemaFragment(FirstQuerySchema.class))); + + GraphQLSchema schema = this.merger.get(); + this.verifySchemaWithQueryFields(schema, Set.of("first", "second")); + this.verifySchemaWithMutationFields(schema, Set.of()); + + GraphQLObjectType sharedType = (GraphQLObjectType) schema.getType("SharedType"); + assertEquals( + "custom", + schema + .getCodeRegistry() + .getDataFetcher(sharedType, sharedType.getFieldDefinition("sharedValue")) + .get(mockDataFetchingEnvironment)); + } + + @Test + void mergesMultipleSchemas() throws Exception { + when(mockRegistry.getRegisteredFragments()) + .thenReturn( + Set.of( + this.createSchemaFragment(FirstQuerySchema.class), + this.createSchemaFragment(SecondQuerySchema.class), + this.createSchemaFragment(ThirdQuerySchema.class))); + + GraphQLSchema schema = this.merger.get(); + this.verifySchemaWithQueryFields(schema, Set.of("first", "second", "third", "fourth")); + + GraphQLFieldDefinition customFetcherField = schema.getQueryType().getFieldDefinition("fourth"); + assertEquals( + "custom", + schema + .getCodeRegistry() + .getDataFetcher(schema.getQueryType(), customFetcherField) + .get(mockDataFetchingEnvironment)); + } + + @Test + void mergesMutationSchemas() { + when(mockRegistry.getRegisteredFragments()) + .thenReturn( + Set.of( + this.createSchemaFragment(FirstQuerySchema.class, FirstMutationSchema.class), + this.createSchemaFragment(SecondQuerySchema.class, SecondMutationSchema.class))); + + this.verifySchemaWithMutationFields(this.merger.get(), Set.of("mutateOne", "mutateTwo")); + } + + @Test + void supportsMutationOnlyFragment() { + when(mockRegistry.getRegisteredFragments()) + .thenReturn( + Set.of( + this.createSchemaFragment(FirstQuerySchema.class), + this.createSchemaFragment(null, FirstMutationSchema.class))); + + this.verifySchemaWithMutationFields(this.merger.get(), Set.of("mutateOne")); + this.verifySchemaWithQueryFields(this.merger.get(), Set.of("first", "second")); + } + + @Test + void supportsTypeFunctions() { + TypeFunction typeFunction = + new TypeFunction() { + @Override + public boolean canBuildType(Class aClass, AnnotatedType annotatedType) { + return aClass == SharedType.class; + } + + @Override + public GraphQLType buildType( + boolean input, + Class aClass, + AnnotatedType annotatedType, + ProcessingElementsContainer container) { + return GraphQLObjectType.newObject() + .name("typeFunctionType") + .field( + GraphQLFieldDefinition.newFieldDefinition() + .name("typeFunctionTypeField") + .type(Scalars.GraphQLString)) + .build(); + } + }; + + when(mockRegistry.getRegisteredFragments()) + .thenReturn( + Set.of(this.createSchemaFragment(FirstQuerySchema.class, null, List.of(typeFunction)))); + + GraphQLSchema schema = this.merger.get(); + this.verifySchemaWithQueryFields(schema, Set.of("first", "second")); + assertEquals( + "typeFunctionType", + ((GraphQLObjectType) schema.getQueryType().getFieldDefinition("second").getType()) + .getName()); + } + + @Test + void supportsEnablingIntrospection() { + when(mockRegistry.getRegisteredFragments()) + .thenReturn(Set.of(this.createSchemaFragment(FirstQuerySchema.class))); + when(mockConfig.isIntrospectionAllowed()).thenReturn(true); + GraphQLSchema schema = this.merger.get(); + assertNotNull(schema.getIntrospectionSchemaType()); + assertFalse( + schema.getCodeRegistry().getFieldVisibility() + instanceof NoIntrospectionGraphqlFieldVisibility); + } + + @Test + void supportsDisablingIntrospection() { + when(mockRegistry.getRegisteredFragments()) + .thenReturn(Set.of(this.createSchemaFragment(FirstQuerySchema.class))); + when(mockConfig.isIntrospectionAllowed()).thenReturn(false); + GraphQLSchema schema = this.merger.get(); + assertInstanceOf( + NoIntrospectionGraphqlFieldVisibility.class, schema.getCodeRegistry().getFieldVisibility()); + } + + private GraphQlSchemaFragment createSchemaFragment(Class queryClass) { + return this.createSchemaFragment(queryClass, null); + } + + private GraphQlSchemaFragment createSchemaFragment(Class queryClass, Class mutationClass) { + return this.createSchemaFragment(queryClass, mutationClass, List.of()); + } + + private GraphQlSchemaFragment createSchemaFragment( + Class queryClass, Class mutationClass, List typeFunctionList) { + return new GraphQlSchemaFragment() { + @Override + public String fragmentName() { + return queryClass.getSimpleName(); + } + + @Override + public Class annotatedQueryClass() { + return queryClass; + } + + @Override + public Class annotatedMutationClass() { + return mutationClass; + } + + @Nonnull + @Override + public List typeFunctions() { + return typeFunctionList; + } + }; + } + + private void verifySchemaWithQueryFields(GraphQLSchema schema, Set fields) { + assertEquals(DefaultSchema.ROOT_QUERY_NAME, schema.getQueryType().getName()); + List fieldDefinitions = schema.getQueryType().getFieldDefinitions(); + assertEquals(fields.size(), fieldDefinitions.size()); + assertTrue( + fields.containsAll( + fieldDefinitions.stream() + .map(GraphQLFieldDefinition::getName) + .collect(Collectors.toUnmodifiableSet()))); + } + + private void verifySchemaWithMutationFields(GraphQLSchema schema, Set fields) { + if (fields.isEmpty()) { + // A type must have fields, so if no fields expected, no type expected + assertNull(schema.getMutationType()); + } else { + assertEquals(ROOT_MUTATION_NAME, schema.getMutationType().getName()); + List fieldDefinitions = + schema.getMutationType().getFieldDefinitions(); + assertEquals(fields.size(), fieldDefinitions.size()); + assertTrue( + fields.containsAll( + fieldDefinitions.stream() + .map(GraphQLFieldDefinition::getName) + .collect(Collectors.toUnmodifiableSet()))); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/build.gradle.kts new file mode 100644 index 00000000..3c5a7dac --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `java-library` + jacoco + alias(commonLibs.plugins.hypertrace.jacoco) +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/gradle.lockfile new file mode 100644 index 00000000..32cda945 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/gradle.lockfile @@ -0,0 +1,44 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.18.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinder.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinder.java new file mode 100644 index 00000000..c89b9a3a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinder.java @@ -0,0 +1,65 @@ +package org.hypertrace.core.graphql.utils.schema; + +import static java.util.function.Predicate.not; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +class DefaultGraphQlSelectionFinder implements GraphQlSelectionFinder { + private static final List ANY_DIRECT_DESCENDANT = List.of(SelectionQuery.ANY); + + @Override + public Stream findSelections( + DataFetchingFieldSelectionSet selectionSet, SelectionQuery query) { + + return this.doSelectionSearch(selectionSet, this.applyQueryDefaults(query)); + } + + private Stream doSelectionSearch( + DataFetchingFieldSelectionSet selectionSet, SelectionQuery query) { + if (query.getSelectionPath().isEmpty()) { + return Stream.empty(); + } + + String expectedName = query.getSelectionPath().get(0); + SelectionQuery childQuery = this.buildQueryForChildSelection(query); + Stream descendantFields = + selectionSet.getFields(SelectionQuery.ANY).stream() + .filter( + field -> + expectedName.equals(SelectionQuery.ANY) + || expectedName.equals(field.getName())); + if (childQuery.getSelectionPath().isEmpty()) { + return descendantFields.filter(query.getMatchesPredicate()); + } + + return descendantFields + .map(SelectedField::getSelectionSet) + .flatMap(childSelectionSet -> this.findSelections(childSelectionSet, childQuery)); + } + + private SelectionQuery applyQueryDefaults(SelectionQuery selectionQuery) { + List pathOrDefault = + Optional.ofNullable(selectionQuery.getSelectionPath()) + .filter(not(List::isEmpty)) + .orElse(ANY_DIRECT_DESCENDANT); + Predicate predicateOrDefault = + Optional.ofNullable(selectionQuery.getMatchesPredicate()).orElse(unused -> true); + + return SelectionQuery.builder() + .selectionPath(pathOrDefault) + .matchesPredicate(predicateOrDefault) + .build(); + } + + private SelectionQuery buildQueryForChildSelection(SelectionQuery selectionQuery) { + return selectionQuery.toBuilder() + .selectionPath( + selectionQuery.getSelectionPath().subList(1, selectionQuery.getSelectionPath().size())) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/GraphQlSelectionFinder.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/GraphQlSelectionFinder.java new file mode 100644 index 00000000..7b415902 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/GraphQlSelectionFinder.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.graphql.utils.schema; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import java.util.stream.Stream; + +public interface GraphQlSelectionFinder { + + Stream findSelections( + DataFetchingFieldSelectionSet selectionSet, SelectionQuery query); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SchemaUtilsModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SchemaUtilsModule.java new file mode 100644 index 00000000..a639f2ae --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SchemaUtilsModule.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.utils.schema; + +import com.google.inject.AbstractModule; + +public class SchemaUtilsModule extends AbstractModule { + @Override + protected void configure() { + bind(GraphQlSelectionFinder.class).to(DefaultGraphQlSelectionFinder.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SelectionQuery.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SelectionQuery.java new file mode 100644 index 00000000..e51669eb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/main/java/org/hypertrace/core/graphql/utils/schema/SelectionQuery.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.graphql.utils.schema; + +import graphql.schema.SelectedField; +import java.util.List; +import java.util.function.Predicate; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class SelectionQuery { + + List selectionPath; + Predicate matchesPredicate; + + public static final String ANY = "*"; + + public static SelectionQuery namedChild(String childName) { + return SelectionQuery.builder().selectionPath(List.of(childName)).build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/test/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/test/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinderTest.java new file mode 100644 index 00000000..96cf7ff6 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-schema-utils/src/test/java/org/hypertrace/core/graphql/utils/schema/DefaultGraphQlSelectionFinderTest.java @@ -0,0 +1,140 @@ +package org.hypertrace.core.graphql.utils.schema; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DefaultGraphQlSelectionFinderTest { + @Mock DataFetchingFieldSelectionSet mockSelectionSet; + @Mock SelectedField mockSelectionFoo; + @Mock SelectedField mockSelectionBar; + + final DefaultGraphQlSelectionFinder selectionFinder = new DefaultGraphQlSelectionFinder(); + private List topLevelSelectionSet; + + @BeforeEach + void beforeEach() { + this.topLevelSelectionSet = List.of(this.mockSelectionFoo, this.mockSelectionBar); + when(this.mockSelectionFoo.getName()).thenReturn("foo"); + when(this.mockSelectionBar.getName()).thenReturn("bar"); + when(this.mockSelectionSet.getFields(any(String.class))) + .thenAnswer(ignored -> topLevelSelectionSet); + } + + @Test + void returnsEmptyStreamForNoMatchingTopLevelSelections() { + assertIterableEquals( + Collections.emptyList(), + this.selectionFinder + .findSelections( + this.mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("no-match")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void returnsMatchingTopLevelSelections() { + assertIterableEquals( + List.of(this.mockSelectionBar), + this.selectionFinder + .findSelections( + mockSelectionSet, SelectionQuery.builder().selectionPath(List.of("bar")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void returnsEmptyForOneLevelMatch() { + DataFetchingFieldSelectionSet fooSelectionSet = mock(DataFetchingFieldSelectionSet.class); + when(fooSelectionSet.getFields(any(String.class))).thenReturn(Collections.emptyList()); + when(this.mockSelectionFoo.getSelectionSet()).thenReturn(fooSelectionSet); + + assertIterableEquals( + Collections.emptyList(), + this.selectionFinder + .findSelections( + mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("foo", "bar")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void returnsMultiLevelMatch() { + DataFetchingFieldSelectionSet fooSelectionSet = mock(DataFetchingFieldSelectionSet.class); + when(fooSelectionSet.getFields(any(String.class))).thenReturn(List.of(this.mockSelectionBar)); + when(this.mockSelectionFoo.getSelectionSet()).thenReturn(fooSelectionSet); + + assertIterableEquals( + List.of(this.mockSelectionBar), + this.selectionFinder + .findSelections( + mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("foo", "bar")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void supportsMultipleMatchFanOut() { + DataFetchingFieldSelectionSet fooSelectionSet = mock(DataFetchingFieldSelectionSet.class); + this.topLevelSelectionSet = List.of(this.mockSelectionFoo, this.mockSelectionFoo); + when(fooSelectionSet.getFields(any(String.class))) + .thenReturn(List.of(this.mockSelectionBar, this.mockSelectionBar)); + when(this.mockSelectionFoo.getSelectionSet()).thenReturn(fooSelectionSet); + + assertIterableEquals( + List.of( + this.mockSelectionBar, + this.mockSelectionBar, + this.mockSelectionBar, + this.mockSelectionBar), + this.selectionFinder + .findSelections( + mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("foo", "bar")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void supportsWildcardPath() { + reset(this.mockSelectionFoo); + reset(this.mockSelectionBar); + assertIterableEquals( + List.of(this.mockSelectionFoo, this.mockSelectionBar), + this.selectionFinder + .findSelections( + mockSelectionSet, SelectionQuery.builder().selectionPath(List.of("*")).build()) + .collect(Collectors.toUnmodifiableList())); + } + + @Test + void supportsLeafPredicates() { + DataFetchingFieldSelectionSet fooSelectionSet = mock(DataFetchingFieldSelectionSet.class); + when(fooSelectionSet.getFields(any(String.class))) + .thenReturn(List.of(this.mockSelectionBar, this.mockSelectionFoo)); + when(this.mockSelectionFoo.getSelectionSet()).thenReturn(fooSelectionSet); + + assertIterableEquals( + List.of(this.mockSelectionBar), + this.selectionFinder + .findSelections( + mockSelectionSet, + SelectionQuery.builder() + .selectionPath(List.of("foo", "*")) + .matchesPredicate(field -> field.getName().equals("bar")) + .build()) + .collect(Collectors.toUnmodifiableList())); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-service/build.gradle.kts new file mode 100644 index 00000000..24b2566f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + java + application + alias(commonLibs.plugins.hypertrace.docker.application) + alias(commonLibs.plugins.hypertrace.docker.publish) +} + +dependencies { + implementation(commonLibs.hypertrace.framework.http) + implementation(commonLibs.slf4j2.api) + + implementation(localLibs.graphql.servlet) + implementation(projects.hypertraceCoreGraphqlImpl) + implementation(projects.hypertraceCoreGraphqlSpi) + implementation(commonLibs.typesafe.config) + + runtimeOnly(commonLibs.log4j.slf4j2.impl) + runtimeOnly(commonLibs.grpc.netty) + + compileOnly(commonLibs.lombok) + annotationProcessor(commonLibs.lombok) +} + +application { + mainClass.set("org.hypertrace.core.serviceframework.PlatformServiceLauncher") +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-service/gradle.lockfile new file mode 100644 index 00000000..832327cb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/gradle.lockfile @@ -0,0 +1,124 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=runtimeClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject.extensions:guice-servlet:5.1.0=runtimeClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java-util:3.24.1=runtimeClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java:3.24.1=runtimeClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.helger:profiler:1.1.1=runtimeClasspath,testRuntimeClasspath +com.typesafe:config:1.4.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-codec:commons-codec:1.15=runtimeClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.2=runtimeClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-core:4.2.16=runtimeClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-healthchecks:4.2.16=runtimeClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-json:4.2.16=runtimeClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-jvm:4.2.16=runtimeClasspath,testRuntimeClasspath +io.dropwizard.metrics:metrics-servlets:4.2.16=runtimeClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.mweirauch:micrometer-jvm-extras:0.2.2=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-netty:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-commons:1.10.2=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-core:1.10.2=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-observation:1.10.2=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.10.2=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-codec-socks:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-codec:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-common:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-handler-proxy:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-handler:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.netty:netty-transport:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-proto:1.1.0-alpha=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.12.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_pushgateway:0.12.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_servlet:0.12.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_servlet_common:0.12.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_tracer_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_tracer_otel:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_tracer_otel_agent:0.16.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=runtimeClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=runtimeClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.xml.bind:jaxb-api:2.3.0=runtimeClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.12.0=runtimeClasspath,testRuntimeClasspath +org.apache.commons:commons-text:1.10.0=runtimeClasspath,testRuntimeClasspath +org.apache.httpcomponents:httpclient:4.5.13=runtimeClasspath,testRuntimeClasspath +org.apache.httpcomponents:httpcore:4.4.13=runtimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.20.0=runtimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-core:2.20.0=runtimeClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0=runtimeClasspath,testRuntimeClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-continuation:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-http:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-io:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-security:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-server:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-servlet:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-servlets:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-util-ajax:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-util:9.4.53.v20231009=runtimeClasspath,testRuntimeClasspath +org.hdrhistogram:HdrHistogram:2.1.12=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.serviceframework:platform-http-service-framework:0.1.73=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.serviceframework:platform-metrics:0.1.73=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.serviceframework:platform-service-framework:0.1.73=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.serviceframework:service-framework-spi:0.1.73=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=runtimeClasspath,testRuntimeClasspath +org.latencyutils:LatencyUtils:2.0.3=runtimeClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,testCompileClasspath +org.reactivestreams:reactive-streams:1.0.4=runtimeClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlEndpointConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlEndpointConfig.java new file mode 100644 index 00000000..49a5ce7f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlEndpointConfig.java @@ -0,0 +1,39 @@ +package org.hypertrace.core.graphql.service; + +import com.typesafe.config.Config; +import java.time.Duration; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; + +@Value +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder(access = AccessLevel.PRIVATE) +class DefaultGraphQlEndpointConfig implements GraphQlEndpointConfig { + private static final String URL_PATH_PROP_KEY = "graphql.urlPath"; + private static final String TIMEOUT_PROP_KEY = "graphql.timeout"; + private static final String MAX_IO_THREADS_PROP_KEY = "threads.io.max"; + private static final String MAX_REQUEST_THREADS_PROP_KEY = "threads.request.max"; + private static final String CORS_ENABLED_PROP_KEY = "graphql.corsEnabled"; + private static final String INTROSPECTION_ENABLED_PROP_KEY = "introspection.enabled"; + + String urlPath; + Duration timeout; + int maxRequestThreads; + int maxIoThreads; + boolean corsEnabled; + boolean introspectionAllowed; + + static GraphQlEndpointConfig fromConfig(Config config) { + return new DefaultGraphQlEndpointConfigBuilder() + .urlPath(config.getString(URL_PATH_PROP_KEY)) + .timeout(config.getDuration(TIMEOUT_PROP_KEY)) + .maxRequestThreads(config.getInt(MAX_REQUEST_THREADS_PROP_KEY)) + .maxIoThreads(config.getInt(MAX_IO_THREADS_PROP_KEY)) + .corsEnabled(config.getBoolean(CORS_ENABLED_PROP_KEY)) + .introspectionAllowed(config.getBoolean(INTROSPECTION_ENABLED_PROP_KEY)) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceConfig.java new file mode 100644 index 00000000..76ced6e7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceConfig.java @@ -0,0 +1,119 @@ +package org.hypertrace.core.graphql.service; + +import com.typesafe.config.Config; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; + +class DefaultGraphQlServiceConfig implements GraphQlServiceConfig { + + private static final Long DEFAULT_CLIENT_TIMEOUT_SECONDS = 10L; + + private static final String SERVICE_NAME_CONFIG = "service.name"; + private static final String SERVICE_PORT_CONFIG = "service.port"; + + private static final String DEFAULT_TENANT_ID = "defaultTenantId"; + + private static final String ATTRIBUTE_SERVICE_HOST_PROPERTY = "attribute.service.host"; + private static final String ATTRIBUTE_SERVICE_PORT_PROPERTY = "attribute.service.port"; + private static final String ATTRIBUTE_SERVICE_CLIENT_TIMEOUT = "attribute.service.timeout"; + + private static final String GATEWAY_SERVICE_HOST_PROPERTY = "gateway.service.host"; + private static final String GATEWAY_SERVICE_PORT_PROPERTY = "gateway.service.port"; + private static final String GATEWAY_SERVICE_CLIENT_TIMEOUT = "gateway.service.timeout"; + private static final String GATEWAY_SERVICE_CLIENT_MAX_INBOUND_MESSAGE_SIZE = + "gateway.service.maxMessageSize.inbound"; + + private final String serviceName; + private final int servicePort; + private final Optional defaultTenantId; + private final String attributeServiceHost; + private final int attributeServicePort; + private final Duration attributeServiceTimeout; + private final String gatewayServiceHost; + private final int gatewayServicePort; + private final Duration gatewayServiceTimeout; + private final int gatewayServiceMaxInboundMessageSize; + + DefaultGraphQlServiceConfig(Config untypedConfig) { + this.serviceName = untypedConfig.getString(SERVICE_NAME_CONFIG); + this.servicePort = untypedConfig.getInt(SERVICE_PORT_CONFIG); + this.defaultTenantId = optionallyGet(() -> untypedConfig.getString(DEFAULT_TENANT_ID)); + + this.attributeServiceHost = untypedConfig.getString(ATTRIBUTE_SERVICE_HOST_PROPERTY); + this.attributeServicePort = untypedConfig.getInt(ATTRIBUTE_SERVICE_PORT_PROPERTY); + this.attributeServiceTimeout = + getTimeoutOrFallback(() -> untypedConfig.getDuration(ATTRIBUTE_SERVICE_CLIENT_TIMEOUT)); + + this.gatewayServiceHost = untypedConfig.getString(GATEWAY_SERVICE_HOST_PROPERTY); + this.gatewayServicePort = untypedConfig.getInt(GATEWAY_SERVICE_PORT_PROPERTY); + this.gatewayServiceTimeout = + getTimeoutOrFallback(() -> untypedConfig.getDuration(GATEWAY_SERVICE_CLIENT_TIMEOUT)); + this.gatewayServiceMaxInboundMessageSize = + untypedConfig.getBytes(GATEWAY_SERVICE_CLIENT_MAX_INBOUND_MESSAGE_SIZE).intValue(); + } + + @Override + public int getServicePort() { + return servicePort; + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public Optional getDefaultTenantId() { + return this.defaultTenantId; + } + + @Override + public String getAttributeServiceHost() { + return this.attributeServiceHost; + } + + @Override + public int getAttributeServicePort() { + return this.attributeServicePort; + } + + @Override + public Duration getAttributeServiceTimeout() { + return attributeServiceTimeout; + } + + @Override + public String getGatewayServiceHost() { + return this.gatewayServiceHost; + } + + @Override + public int getGatewayServicePort() { + return this.gatewayServicePort; + } + + @Override + public Duration getGatewayServiceTimeout() { + return gatewayServiceTimeout; + } + + @Override + public int getGatewayServiceMaxInboundMessageSize() { + return this.gatewayServiceMaxInboundMessageSize; + } + + private Duration getTimeoutOrFallback(Supplier durationSupplier) { + return optionallyGet(durationSupplier) + .orElse(Duration.ofSeconds(DEFAULT_CLIENT_TIMEOUT_SECONDS)); + } + + private Optional optionallyGet(Supplier valueSupplier) { + try { + return Optional.ofNullable(valueSupplier.get()); + } catch (Throwable unused) { + return Optional.empty(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceLifecycle.java b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceLifecycle.java new file mode 100644 index 00000000..92bc976b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/DefaultGraphQlServiceLifecycle.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.service; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.hypertrace.core.graphql.spi.lifecycle.GraphQlServiceLifecycle; + +class DefaultGraphQlServiceLifecycle implements GraphQlServiceLifecycle { + private final CompletableFuture completableFuture = new CompletableFuture<>(); + + @Override + public CompletionStage shutdownCompletion() { + return this.completableFuture.minimalCompletionStage(); + } + + void shutdown() { + this.completableFuture.complete(null); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlService.java b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlService.java new file mode 100644 index 00000000..33654cd3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlService.java @@ -0,0 +1,56 @@ +package org.hypertrace.core.graphql.service; + +import com.typesafe.config.Config; +import java.util.List; +import org.hypertrace.core.graphql.impl.GraphQlFactory; +import org.hypertrace.core.graphql.spi.config.GraphQlEndpointConfig; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.serviceframework.config.ConfigClient; +import org.hypertrace.core.serviceframework.http.HttpContainerEnvironment; +import org.hypertrace.core.serviceframework.http.HttpHandlerDefinition; +import org.hypertrace.core.serviceframework.http.HttpHandlerDefinition.CorsConfig; +import org.hypertrace.core.serviceframework.http.HttpHandlerFactory; +import org.hypertrace.core.serviceframework.http.StandAloneHttpPlatformServiceContainer; + +public class GraphQlService extends StandAloneHttpPlatformServiceContainer { + private static final String SERVICE_NAME = "hypertrace-core-graphql"; + + public GraphQlService(ConfigClient configClient) { + super(configClient); + } + + @Override + protected List getHandlerFactories() { + return List.of(this::buildHandlerDefinition); + } + + List buildHandlerDefinition(HttpContainerEnvironment environment) { + Config rawConfig = environment.getConfig(SERVICE_NAME); + GraphQlServiceConfig serviceConfig = new DefaultGraphQlServiceConfig(rawConfig); + GraphQlEndpointConfig endpointConfig = DefaultGraphQlEndpointConfig.fromConfig(rawConfig); + DefaultGraphQlServiceLifecycle serviceLifecycle = new DefaultGraphQlServiceLifecycle(); + environment.getLifecycle().shutdownComplete().thenRun(serviceLifecycle::shutdown); + + return List.of( + HttpHandlerDefinition.builder() + .name("graphql") + .port(serviceConfig.getServicePort()) + .contextPath(endpointConfig.getUrlPath()) + .corsConfig(buildCorsConfig(endpointConfig)) + .servlet( + new GraphQlServiceHttpServlet( + GraphQlFactory.build( + serviceConfig, + endpointConfig, + serviceLifecycle, + environment.getChannelRegistry()))) + .build()); + } + + private CorsConfig buildCorsConfig(GraphQlEndpointConfig config) { + if (!config.isCorsEnabled()) { + return null; + } + return CorsConfig.builder().allowedHeaders(List.of("*")).build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlServiceHttpServlet.java b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlServiceHttpServlet.java new file mode 100644 index 00000000..67635758 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/java/org/hypertrace/core/graphql/service/GraphQlServiceHttpServlet.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.service; + +import graphql.kickstart.servlet.GraphQLConfiguration; +import graphql.kickstart.servlet.GraphQLHttpServlet; + +class GraphQlServiceHttpServlet extends GraphQLHttpServlet { + + private final GraphQLConfiguration configuration; + + GraphQlServiceHttpServlet(GraphQLConfiguration configuration) { + this.configuration = configuration; + } + + @Override + protected GraphQLConfiguration getConfiguration() { + return configuration; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/configs/common/application.conf b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/configs/common/application.conf new file mode 100644 index 00000000..2c301472 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/configs/common/application.conf @@ -0,0 +1,25 @@ +main.class = org.hypertrace.core.graphql.service.GraphQlService +service.name = graphql-service +service.port = 23431 #TODO resolve with existing graphql port when ready for deployment +service.admin.port = 23432 + +graphql.urlPath = /graphql +graphql.corsEnabled = true +graphql.timeout = 30s +introspection.enabled = false + +threads.io.max = 10 +threads.request.max = 10 + +attribute.service = { + host = localhost + port = 9012 +} + +gateway.service = { + host = localhost + port = 50071 + maxMessageSize = { + inbound = 4MiB + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/log4j2.properties b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/log4j2.properties new file mode 100644 index 00000000..62c371c3 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-service/src/main/resources/log4j2.properties @@ -0,0 +1,8 @@ +status=error +name=PropertiesConfig +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c{1.} - %msg%n +rootLogger.level=INFO +rootLogger.appenderRef.stdout.ref=STDOUT diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/build.gradle.kts new file mode 100644 index 00000000..ba4fd0c4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(projects.hypertraceCoreGraphqlSpi) + api(localLibs.graphql.annotations) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + compileOnly(projects.hypertraceCoreGraphqlAttributeScopeConstants) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.rxjava3) + implementation(commonLibs.hypertrace.gatewayservice.api) + implementation(commonLibs.protobuf.javautil) + implementation(localLibs.opentelemetry.proto) + implementation(commonLibs.commons.text) + + implementation(projects.hypertraceCoreGraphqlContext) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + implementation(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlAttributeStore) + implementation(projects.hypertraceCoreGraphqlLogEventSchema) + implementation(projects.hypertraceCoreGraphqlDeserialization) + implementation(projects.hypertraceCoreGraphqlSchemaUtils) + implementation(projects.hypertraceCoreGraphqlAttributeScopeConstants) + implementation(projects.hypertraceCoreGraphqlRequestTransformation) + + testImplementation(commonLibs.junit.jupiter) + testImplementation(commonLibs.jackson.databind) + testImplementation(projects.hypertraceCoreGraphqlGatewayServiceUtils) + testImplementation(commonLibs.mockito.core) + testImplementation(commonLibs.mockito.junit) + + testAnnotationProcessor(commonLibs.lombok) + testCompileOnly(commonLibs.lombok) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/gradle.lockfile new file mode 100644 index 00000000..a6957448 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/gradle.lockfile @@ -0,0 +1,86 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.8.9=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java-util:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.opentelemetry:opentelemetry-proto:1.1.0-alpha=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.12.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-text:1.10.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-api:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter-params:5.10.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.10.0=testCompileClasspath +org.junit.jupiter:junit-jupiter:5.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath +org.junit.platform:junit-platform-commons:1.10.1=testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath +org.junit:junit-bom:5.10.1=testRuntimeClasspath +org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.8.0=testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath,testCompileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaFragment.java new file mode 100644 index 00000000..29a16654 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaFragment.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.span; + +import org.hypertrace.core.graphql.span.schema.SpanSchema; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class SpanSchemaFragment implements GraphQlSchemaFragment { + + @Override + public String fragmentName() { + return "Span schema"; + } + + @Override + public Class annotatedQueryClass() { + return SpanSchema.class; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaModule.java new file mode 100644 index 00000000..f677143c --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/SpanSchemaModule.java @@ -0,0 +1,23 @@ +package org.hypertrace.core.graphql.span; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.span.dao.SpanDaoModule; +import org.hypertrace.core.graphql.span.joiner.SpanJoinerModule; +import org.hypertrace.core.graphql.span.request.SpanRequestModule; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; + +public class SpanSchemaModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), GraphQlSchemaFragment.class) + .addBinding() + .to(SpanSchemaFragment.class); + + requireBinding(ResultSetRequestBuilder.class); + install(new SpanDaoModule()); + install(new SpanRequestModule()); + install(new SpanJoinerModule()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/ExportSpanDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/ExportSpanDao.java new file mode 100644 index 00000000..fe60f414 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/ExportSpanDao.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.span.dao; + +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.span.export.ExportSpan; +import org.hypertrace.core.graphql.span.export.ExportSpanConverter; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.ExportSpanResult; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; + +public class ExportSpanDao { + private final SpanDao spanDao; + + @Inject + ExportSpanDao(SpanDao spanDao) { + this.spanDao = spanDao; + } + + public Single getSpans(SpanRequest request) { + return this.spanDao + .getSpans(request) + .flatMap(spanResultSet -> this.buildResponse(spanResultSet)); + } + + private Single buildResponse(SpanResultSet result) throws Exception { + List exportSpans = + result.results().stream() + .map(span -> new ExportSpan.Builder(span).build()) + .collect(Collectors.toList()); + return Single.just(new ExportSpanResultImpl(ExportSpanConverter.toJson(exportSpans))); + } + + @lombok.Value + @Accessors(fluent = true) + private static class ExportSpanResultImpl implements ExportSpanResult { + String result; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanConverter.java new file mode 100644 index 00000000..beda91fc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanConverter.java @@ -0,0 +1,98 @@ +package org.hypertrace.core.graphql.span.dao; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.Span; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.span.SpanEvent; + +class GatewayServiceSpanConverter { + + private final BiConverter< + Collection, Map, Map> + attributeMapConverter; + + @Inject + GatewayServiceSpanConverter( + BiConverter< + Collection, Map, Map> + attributeMapConverter) { + this.attributeMapConverter = attributeMapConverter; + } + + public Single convert(SpanRequest request, SpanLogEventsResponse response) { + int total = response.spansResponse().hasTotal() ? response.spansResponse().getTotal() : 0; + + return Observable.fromIterable(response.spansResponse().getSpansList()) + .flatMapSingle(spanEvent -> this.convert(request, spanEvent, response.spanIdToLogEvents())) + .toList() + .map(spans -> new ConvertedSpanResultSet(spans, total, spans.size())); + } + + private Single convert( + SpanRequest request, SpanEvent spanEvent, Map> spanIdToLogEvents) { + return this.attributeMapConverter + .convert(request.spanEventsRequest().attributes(), spanEvent.getAttributesMap()) + .map( + attrMap -> + new ConvertedSpan( + attrMap + .get( + request + .spanEventsRequest() + .idAttribute() + .attributeExpressionAssociation() + .value()) + .toString(), + attrMap, + spanIdToLogEvents)); + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedSpan implements Span { + String id; + Map attributeValues; + Map> spanIdToLogEvents; + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + + @Override + public LogEventResultSet logEvents() { + List list = spanIdToLogEvents.getOrDefault(id, Collections.emptyList()); + return new ConvertedLogEventResultSet(list, list.size(), list.size()); + } + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedSpanResultSet implements SpanResultSet { + List results; + long total; + long count; + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedLogEventResultSet implements LogEventResultSet { + List results; + long total; + long count; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanDao.java new file mode 100644 index 00000000..7fc406d9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanDao.java @@ -0,0 +1,70 @@ +package org.hypertrace.core.graphql.span.dao; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.reactivex.rxjava3.core.Single; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.request.transformation.RequestTransformer; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.span.SpansRequest; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +@Singleton +class GatewayServiceSpanDao implements SpanDao { + + private final GatewayServiceFutureStub gatewayServiceStub; + private final GrpcContextBuilder grpcContextBuilder; + private final GatewayServiceSpanRequestBuilder requestBuilder; + private final GatewayServiceSpanConverter spanConverter; + private final SpanLogEventDao spanLogEventDao; + private final GraphQlServiceConfig serviceConfig; + private final RequestTransformer requestTransformer; + + @Inject + GatewayServiceSpanDao( + GraphQlServiceConfig serviceConfig, + GatewayServiceFutureStub gatewayServiceFutureStub, + GrpcContextBuilder grpcContextBuilder, + GatewayServiceSpanRequestBuilder requestBuilder, + GatewayServiceSpanConverter spanConverter, + SpanLogEventDao spanLogEventDao, + RequestTransformer requestTransformer) { + this.grpcContextBuilder = grpcContextBuilder; + this.requestBuilder = requestBuilder; + this.spanConverter = spanConverter; + this.spanLogEventDao = spanLogEventDao; + this.gatewayServiceStub = gatewayServiceFutureStub; + this.serviceConfig = serviceConfig; + this.requestTransformer = requestTransformer; + } + + @Override + public Single getSpans(SpanRequest request) { + return this.requestTransformer + .transform(request) + .flatMap(this.requestBuilder::buildRequest) + .flatMap( + serverRequest -> this.makeRequest(request.spanEventsRequest().context(), serverRequest)) + .flatMap(serverResponse -> spanLogEventDao.fetchLogEvents(request, serverResponse)) + .flatMap( + spanLogEventsResponse -> this.spanConverter.convert(request, spanLogEventsResponse)); + } + + private Single makeRequest(GraphQlRequestContext context, SpansRequest request) { + return Single.fromFuture( + this.grpcContextBuilder + .build(context) + .call( + () -> + this.gatewayServiceStub + .withDeadlineAfter( + serviceConfig.getGatewayServiceTimeout().toMillis(), MILLISECONDS) + .getSpans(request))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanRequestBuilder.java new file mode 100644 index 00000000..1445d3a9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/GatewayServiceSpanRequestBuilder.java @@ -0,0 +1,62 @@ +package org.hypertrace.core.graphql.span.dao; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.span.SpansRequest; + +class GatewayServiceSpanRequestBuilder { + + private final Converter>, Filter> filterConverter; + private final Converter>, List> + orderConverter; + private final Converter, Set> attributeConverter; + + @Inject + GatewayServiceSpanRequestBuilder( + Converter>, Filter> filterConverter, + Converter>, List> orderConverter, + Converter, Set> attributeConverter) { + this.filterConverter = filterConverter; + this.orderConverter = orderConverter; + this.attributeConverter = attributeConverter; + } + + Single buildRequest(SpanRequest gqlRequest) { + return zip( + this.attributeConverter.convert(gqlRequest.spanEventsRequest().attributes()), + this.orderConverter.convert(gqlRequest.spanEventsRequest().orderArguments()), + this.filterConverter.convert(gqlRequest.spanEventsRequest().filterArguments()), + (selections, orderBys, filters) -> + SpansRequest.newBuilder() + .setStartTimeMillis( + gqlRequest.spanEventsRequest().timeRange().startTime().toEpochMilli()) + .setEndTimeMillis( + gqlRequest.spanEventsRequest().timeRange().endTime().toEpochMilli()) + .addAllSelection(selections) + .addAllOrderBy(orderBys) + .setLimit(gqlRequest.spanEventsRequest().limit()) + .setOffset(gqlRequest.spanEventsRequest().offset()) + .setFilter(filters) + .setSpaceId( + gqlRequest + .spanEventsRequest() + .spaceId() + .orElse("")) // String proto default value + .setFetchTotal(gqlRequest.fetchTotal()) + .build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDao.java new file mode 100644 index 00000000..7e55f74b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDao.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.span.dao; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; + +public interface SpanDao { + + Single getSpans(SpanRequest request); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDaoModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDaoModule.java new file mode 100644 index 00000000..ce172161 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanDaoModule.java @@ -0,0 +1,71 @@ +package org.hypertrace.core.graphql.span.dao; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.request.transformation.RequestTransformer; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.common.Value; + +public class SpanDaoModule extends AbstractModule { + + @Override + protected void configure() { + bind(SpanDao.class).to(GatewayServiceSpanDao.class); + requireBinding(GatewayServiceFutureStub.class); + requireBinding(GraphQlServiceConfig.class); + requireBinding(CallCredentials.class); + requireBinding(GraphQlServiceConfig.class); + requireBinding(GrpcContextBuilder.class); + requireBinding(GrpcChannelRegistry.class); + requireBinding(FilterRequestBuilder.class); + requireBinding(ArgumentDeserializer.class); + requireBinding(AttributeStore.class); + requireBinding(RequestTransformer.class); + requireBinding(AttributeRequestBuilder.class); + + requireBinding( + Key.get(new TypeLiteral, Set>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter< + List>, List>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter>, Filter>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + BiConverter< + Collection, + Map, + Map>>() {})); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventDao.java new file mode 100644 index 00000000..f49d92e0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventDao.java @@ -0,0 +1,83 @@ +package org.hypertrace.core.graphql.span.dao; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.reactivex.rxjava3.core.Single; +import java.util.Map; +import javax.inject.Inject; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +class SpanLogEventDao { + + private final GatewayServiceFutureStub gatewayServiceStub; + private final GrpcContextBuilder grpcContextBuilder; + private final SpanLogEventRequestBuilder spanLogEventRequestBuilder; + private final SpanLogEventResponseConverter spanLogEventResponseConverter; + private final GraphQlServiceConfig serviceConfig; + + @Inject + SpanLogEventDao( + GraphQlServiceConfig serviceConfig, + GatewayServiceFutureStub gatewayServiceFutureStub, + GrpcContextBuilder grpcContextBuilder, + SpanLogEventRequestBuilder spanLogEventRequestBuilder, + SpanLogEventResponseConverter spanLogEventResponseConverter) { + this.gatewayServiceStub = gatewayServiceFutureStub; + this.grpcContextBuilder = grpcContextBuilder; + this.spanLogEventRequestBuilder = spanLogEventRequestBuilder; + this.spanLogEventResponseConverter = spanLogEventResponseConverter; + this.serviceConfig = serviceConfig; + } + + /** + * + * + *

    + *
  • 1. Fetch log event attributes from {@code gqlRequest} + *
  • 2. Build log event request using attribute and spanIds as filter + *
  • 3. Query log events + *
  • 4. Processed log events response to build mapping from spanId to logEvent + *
+ */ + Single fetchLogEvents( + SpanRequest gqlRequest, SpansResponse spansResponse) { + if (null == gqlRequest.spanEventsRequest().idAttribute() + || null == gqlRequest.logEventAttributes() + || gqlRequest.logEventAttributes().isEmpty() + || spansResponse.getSpansList().isEmpty()) { + return Single.just(new SpanLogEventsResponse(spansResponse, Map.of())); + } + return spanLogEventRequestBuilder + .buildLogEventsRequest(gqlRequest, spansResponse) + .flatMap( + logEventsRequest -> + makeRequest(gqlRequest.spanEventsRequest().context(), logEventsRequest)) + .flatMap( + logEventsResponse -> + spanLogEventResponseConverter.buildResponse( + gqlRequest.spanEventsRequest().context(), + gqlRequest.logEventAttributes(), + spansResponse, + logEventsResponse)); + } + + private Single makeRequest( + GraphQlRequestContext context, LogEventsRequest request) { + return Single.fromFuture( + this.grpcContextBuilder + .build(context) + .call( + () -> + this.gatewayServiceStub + .withDeadlineAfter( + serviceConfig.getGatewayServiceTimeout().toMillis(), MILLISECONDS) + .getLogEvents(request))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilder.java new file mode 100644 index 00000000..ddeaaa97 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilder.java @@ -0,0 +1,118 @@ +package org.hypertrace.core.graphql.span.dao; + +import static io.reactivex.rxjava3.core.Single.zip; + +import com.google.common.collect.ImmutableSet; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +class SpanLogEventRequestBuilder { + private static final int LOG_EVENT_TOTAL_LIMIT = 1000; + + private final Converter, Set> attributeConverter; + private final Converter>, Filter> filterConverter; + private final FilterRequestBuilder filterRequestBuilder; + private final AttributeStore attributeStore; + private final AttributeRequestBuilder attributeRequestBuilder; + + @Inject + SpanLogEventRequestBuilder( + Converter, Set> attributeConverter, + Converter>, Filter> filterConverter, + FilterRequestBuilder filterRequestBuilder, + AttributeStore attributeStore, + AttributeRequestBuilder attributeRequestBuilder) { + this.attributeConverter = attributeConverter; + this.filterConverter = filterConverter; + this.filterRequestBuilder = filterRequestBuilder; + this.attributeStore = attributeStore; + this.attributeRequestBuilder = attributeRequestBuilder; + } + + Single buildLogEventsRequest( + SpanRequest gqlRequest, SpansResponse spansResponse) { + return zip( + getRequestAttributes( + gqlRequest.spanEventsRequest().context(), gqlRequest.logEventAttributes()), + buildLogEventsQueryFilter(gqlRequest, spansResponse).flatMap(filterConverter::convert), + (selections, filter) -> + LogEventsRequest.newBuilder() + .setStartTimeMillis( + gqlRequest.spanEventsRequest().timeRange().startTime().toEpochMilli()) + .setEndTimeMillis( + gqlRequest.spanEventsRequest().timeRange().endTime().toEpochMilli()) + .addAllSelection(selections) + .setLimit(LOG_EVENT_TOTAL_LIMIT) + .setFilter(filter) + .build()); + } + + private Single> getRequestAttributes( + GraphQlRequestContext requestContext, Collection logEventAttributes) { + return this.attributeStore + .getForeignIdAttribute( + requestContext, + HypertraceCoreAttributeScopeString.LOG_EVENT, + HypertraceCoreAttributeScopeString.SPAN) + .map(attributeRequestBuilder::buildForAttribute) + .toObservable() + .concatWith(Observable.fromIterable(logEventAttributes)) + .collect(ImmutableSet.toImmutableSet()) + .flatMap(attributeConverter::convert); + } + + private Single>> buildLogEventsQueryFilter( + SpanRequest gqlRequest, SpansResponse spansResponse) { + List spanIds = + spansResponse.getSpansList().stream() + .map( + spanEvent -> + spanEvent + .getAttributesMap() + .get(gqlRequest.spanEventsRequest().idAttribute().asMapKey()) + .getString()) + .collect(Collectors.toList()); + + return filterRequestBuilder.build( + gqlRequest.spanEventsRequest().context(), + HypertraceCoreAttributeScopeString.LOG_EVENT, + Set.of(new LogEventFilter(spanIds))); + } + + @Value + @Accessors(fluent = true) + private static class LogEventFilter implements FilterArgument { + FilterType type = FilterType.ID; + String key = null; + AttributeExpression keyExpression = null; + FilterOperatorType operator = FilterOperatorType.IN; + Collection value; + AttributeScope idType = null; + String idScope = HypertraceCoreAttributeScopeString.SPAN; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverter.java new file mode 100644 index 00000000..3da5cc1b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverter.java @@ -0,0 +1,110 @@ +package org.hypertrace.core.graphql.span.dao; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +class SpanLogEventResponseConverter { + + private final BiConverter< + Collection, Map, Map> + attributeMapConverter; + private final AttributeStore attributeStore; + private final AttributeRequestBuilder attributeRequestBuilder; + + @Inject + SpanLogEventResponseConverter( + BiConverter< + Collection, Map, Map> + attributeMapConverter, + AttributeStore attributeStore, + AttributeRequestBuilder attributeRequestBuilder) { + this.attributeMapConverter = attributeMapConverter; + this.attributeStore = attributeStore; + this.attributeRequestBuilder = attributeRequestBuilder; + } + + Single buildResponse( + GraphQlRequestContext graphQlRequestContext, + Collection attributeRequests, + SpansResponse spansResponse, + LogEventsResponse logEventsResponse) { + return this.attributeStore + .getForeignIdAttribute( + graphQlRequestContext, + HypertraceCoreAttributeScopeString.LOG_EVENT, + HypertraceCoreAttributeScopeString.SPAN) + .map(this.attributeRequestBuilder::buildForAttribute) + .flatMap( + spanIdRequest -> + buildResponse(spanIdRequest, attributeRequests, spansResponse, logEventsResponse)); + } + + private Single buildResponse( + AttributeRequest foreignIdAttributeRequest, + Collection attributeRequests, + SpansResponse spansResponse, + LogEventsResponse logEventsResponse) { + return Observable.fromIterable(logEventsResponse.getLogEventsList()) + .concatMapSingle( + logEventsResponseVar -> + this.convert(foreignIdAttributeRequest, attributeRequests, logEventsResponseVar)) + .collect( + Collectors.groupingBy( + SpanLogEventPair::spanId, + Collectors.mapping(SpanLogEventPair::logEvent, Collectors.toList()))) + .map( + spanIdVsLogEventsMap -> new SpanLogEventsResponse(spansResponse, spanIdVsLogEventsMap)); + } + + private Single convert( + AttributeRequest foreignIdAttributeRequest, + Collection request, + org.hypertrace.gateway.service.v1.log.events.LogEvent logEvent) { + return this.attributeMapConverter + .convert(request, logEvent.getAttributesMap()) + .map( + attributeMap -> + new SpanLogEventPair( + logEvent + .getAttributesMap() + .get(foreignIdAttributeRequest.asMapKey()) + .getString(), + new ConvertedLogEvent(attributeMap))); + } + + @lombok.Value + @Accessors(fluent = true) + private static class SpanLogEventPair { + String spanId; + LogEvent logEvent; + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedLogEvent + implements org.hypertrace.core.graphql.log.event.schema.LogEvent { + + Map attributeValues; + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventsResponse.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventsResponse.java new file mode 100644 index 00000000..947b1fa9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/dao/SpanLogEventsResponse.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.span.dao; + +import java.util.List; +import java.util.Map; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +@lombok.Value +@Accessors(fluent = true) +class SpanLogEventsResponse { + + SpansResponse spansResponse; + Map> spanIdToLogEvents; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpan.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpan.java new file mode 100644 index 00000000..68ed97dd --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpan.java @@ -0,0 +1,197 @@ +package org.hypertrace.core.graphql.span.export; + +import static org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression.forAttributeKey; +import static org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanTagsKey.SERVICE_NAME_KEY; +import static org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanTagsKey.SPAN_KIND; + +import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.resource.v1.Resource; +import io.opentelemetry.proto.trace.v1.InstrumentationLibrarySpans; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Span.Event; +import io.opentelemetry.proto.trace.v1.Span.SpanKind; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.proto.trace.v1.Status.StatusCode; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.LogEventAttributes; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanAttributes; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanTagsKey; + +@Value +@Accessors(fluent = true) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class ExportSpan { + + ResourceSpans resourceSpans; + + public static class Builder { + + private final org.hypertrace.core.graphql.span.schema.Span span; + + public Builder(org.hypertrace.core.graphql.span.schema.Span span) { + this.span = span; + } + + private void setResourceServiceName(Resource.Builder resourceBuilder) { + Optional.ofNullable(span.attribute(forAttributeKey(SpanAttributes.SERVICE_NAME))) + .map(Object::toString) + .map( + serviceName -> + KeyValue.newBuilder() + .setKey(SERVICE_NAME_KEY) + .setValue(AnyValue.newBuilder().setStringValue(serviceName))) + .map(KeyValue.Builder::build) + .ifPresent(resourceBuilder::addAttributes); + } + + private void setBytesFields(Span.Builder spanBuilder) { + byte[] spanIdBytes = BaseEncoding.base64().decode(span.id()); + spanBuilder.setSpanId(ByteString.copyFrom(spanIdBytes)); + + String traceId = span.attribute(forAttributeKey(SpanAttributes.TRACE_ID)).toString(); + byte[] traceIdBytes = BaseEncoding.base64().decode(traceId); + spanBuilder.setTraceId(ByteString.copyFrom(traceIdBytes)); + + byte[] parentSpanIdBytes = BaseEncoding.base64().decode(""); + if (span.attribute(forAttributeKey(SpanAttributes.PARENT_SPAN_ID)) != null) { + parentSpanIdBytes = + BaseEncoding.base64() + .decode(span.attribute(forAttributeKey(SpanAttributes.PARENT_SPAN_ID)).toString()); + } + spanBuilder.setParentSpanId(ByteString.copyFrom(parentSpanIdBytes)); + } + + private void setTimeFields(Span.Builder spanBuilder) { + long startTime = + Long.parseLong(span.attribute(forAttributeKey(SpanAttributes.START_TIME)).toString()); + spanBuilder.setStartTimeUnixNano( + TimeUnit.NANOSECONDS.convert(startTime, TimeUnit.MILLISECONDS)); + + long endTime = + Long.parseLong(span.attribute(forAttributeKey(SpanAttributes.END_TIME)).toString()); + spanBuilder.setEndTimeUnixNano(TimeUnit.NANOSECONDS.convert(endTime, TimeUnit.MILLISECONDS)); + } + + private void setName(Span.Builder spanBuilder) { + Optional.ofNullable(span.attribute(forAttributeKey(SpanAttributes.NAME))) + .map(Object::toString) + .ifPresent(spanBuilder::setName); + } + + private static void setAttributes(Span.Builder spanBuilder, Map tags) { + List attributes = + tags.entrySet().stream() + .filter(e -> !SpanTagsKey.EXCLUDE_KEYS.contains(e.getKey())) + .map( + e -> + KeyValue.newBuilder() + .setKey(e.getKey()) + .setValue(AnyValue.newBuilder().setStringValue(e.getValue()).build()) + .build()) + .collect(Collectors.toList()); + spanBuilder.addAllAttributes(attributes); + } + + private static void setStatusCode(Span.Builder spanBuilder, Map tags) { + int statusCode = + SpanTagsKey.STATUS_CODE_KEYS.stream() + .filter(tags::containsKey) + .map(e -> Integer.parseInt(tags.get(e))) + .findFirst() + .orElse(0); + spanBuilder.setStatus(Status.newBuilder().setCode(StatusCode.forNumber(statusCode)).build()); + } + + private static void setSpanKind(Span.Builder spanBuilder, Map tags) { + String spanKind = tags.get(SPAN_KIND); + if (spanKind != null) { + spanBuilder.setKind( + SpanKind.valueOf(String.join("_", "SPAN_KIND", spanKind.toUpperCase()))); + } else { + spanBuilder.setKind(SpanKind.SPAN_KIND_UNSPECIFIED); + } + } + + private static void setLogEvent(Span.Builder spanBuilder, List logEvents) { + logEvents.stream() + .forEach( + logEvent -> { + Span.Event.Builder eventBuilder = Event.newBuilder(); + long timeNanos = + Duration.between( + Instant.EPOCH, + Instant.parse( + logEvent + .attribute(forAttributeKey(LogEventAttributes.TIMESTAMP)) + .toString())) + .toNanos(); + eventBuilder.setTimeUnixNano(timeNanos); + + Map logEventAttributes = + Optional.ofNullable( + logEvent.attribute(forAttributeKey(LogEventAttributes.ATTRIBUTES))) + .map(value -> (Map) value) + .orElseGet(Collections::emptyMap); + List attributes = + logEventAttributes.entrySet().stream() + .map( + e -> + KeyValue.newBuilder() + .setKey(e.getKey()) + .setValue( + AnyValue.newBuilder().setStringValue(e.getValue()).build()) + .build()) + .collect(Collectors.toList()); + eventBuilder.addAllAttributes(attributes); + spanBuilder.addEvents(eventBuilder.build()); + }); + } + + public ExportSpan build() { + + ResourceSpans.Builder resourceSpansBuilder = ResourceSpans.newBuilder(); + Resource.Builder resourceBuilder = Resource.newBuilder(); + InstrumentationLibrarySpans.Builder instrumentationLibrarySpansBuilder = + InstrumentationLibrarySpans.newBuilder(); + Span.Builder spanBuilder = Span.newBuilder(); + + setResourceServiceName(resourceBuilder); + setBytesFields(spanBuilder); + setTimeFields(spanBuilder); + setName(spanBuilder); + + Map tags = + Optional.ofNullable(span.attribute(forAttributeKey(SpanAttributes.TAGS))) + .map(value -> (Map) value) + .orElseGet(Collections::emptyMap); + setStatusCode(spanBuilder, tags); + setSpanKind(spanBuilder, tags); + setAttributes(spanBuilder, tags); + + setLogEvent(spanBuilder, span.logEvents().results()); + + resourceSpansBuilder.setResource(resourceBuilder.build()); + instrumentationLibrarySpansBuilder.addSpans(spanBuilder.build()); + resourceSpansBuilder.addInstrumentationLibrarySpans( + instrumentationLibrarySpansBuilder.build()); + + return new ExportSpan(resourceSpansBuilder.build()); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConstants.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConstants.java new file mode 100644 index 00000000..2db0b47d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConstants.java @@ -0,0 +1,48 @@ +package org.hypertrace.core.graphql.span.export; + +import java.util.List; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; + +public interface ExportSpanConstants { + interface SpanAttributes { + String ID = "id"; + String SERVICE_NAME = "serviceName"; + String TRACE_ID = "traceId"; + String PARENT_SPAN_ID = "parentSpanId"; + String START_TIME = "startTime"; + String END_TIME = "endTime"; + String NAME = "displaySpanName"; + String TAGS = "spanTags"; + + List SPAN_ATTRIBUTES = + List.of( + AttributeExpression.forAttributeKey(SpanAttributes.ID), + AttributeExpression.forAttributeKey(SpanAttributes.SERVICE_NAME), + AttributeExpression.forAttributeKey(SpanAttributes.TRACE_ID), + AttributeExpression.forAttributeKey(SpanAttributes.PARENT_SPAN_ID), + AttributeExpression.forAttributeKey(SpanAttributes.START_TIME), + AttributeExpression.forAttributeKey(SpanAttributes.END_TIME), + AttributeExpression.forAttributeKey(SpanAttributes.NAME), + AttributeExpression.forAttributeKey(SpanAttributes.TAGS)); + } + + interface SpanTagsKey { + String SERVICE_NAME_KEY = "service.name"; + String SPAN_KIND = "span.kind"; + String STATUS_CODE = "status.code"; + String ERROR = "error"; + + List EXCLUDE_KEYS = List.of(SPAN_KIND, STATUS_CODE, ERROR); + List STATUS_CODE_KEYS = List.of(STATUS_CODE, ERROR); + } + + interface LogEventAttributes { + String TIMESTAMP = "timestamp"; + String ATTRIBUTES = "attributes"; + + List LOG_EVENT_ATTRIBUTES = + List.of( + AttributeExpression.forAttributeKey(LogEventAttributes.TIMESTAMP), + AttributeExpression.forAttributeKey(LogEventAttributes.ATTRIBUTES)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConverter.java new file mode 100644 index 00000000..9e6f73e7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/export/ExportSpanConverter.java @@ -0,0 +1,49 @@ +package org.hypertrace.core.graphql.span.export; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import lombok.experimental.Accessors; +import org.apache.commons.text.StringEscapeUtils; + +@lombok.Value +@Accessors(fluent = true) +public class ExportSpanConverter { + + private static final ObjectMapper OBJECT_MAPPER = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private static class MessageSerializer extends JsonSerializer { + private static final JsonFormat.Printer PRINTER = + JsonFormat.printer().omittingInsignificantWhitespace(); + + @Override + public void serialize(Message message, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + generator.writeRawValue(PRINTER.print(message)); + } + } + + @JsonSerialize(contentUsing = MessageSerializer.class) + List resourceSpans; + + private String toJson() throws JsonProcessingException { + return StringEscapeUtils.unescapeJson(OBJECT_MAPPER.writeValueAsString(this)); + } + + public static String toJson(List exportSpans) throws JsonProcessingException { + List spans = + exportSpans.stream().map(s -> s.resourceSpans()).collect(Collectors.toList()); + return new ExportSpanConverter(spans).toJson(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/ExportSpanFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/ExportSpanFetcher.java new file mode 100644 index 00000000..761e217e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/ExportSpanFetcher.java @@ -0,0 +1,50 @@ +package org.hypertrace.core.graphql.span.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import io.reactivex.rxjava3.core.Single; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.fetcher.InjectableDataFetcher; +import org.hypertrace.core.graphql.span.dao.ExportSpanDao; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.LogEventAttributes; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanAttributes; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.request.SpanRequestBuilder; +import org.hypertrace.core.graphql.span.schema.ExportSpanResult; + +public class ExportSpanFetcher extends InjectableDataFetcher { + + public ExportSpanFetcher() { + super(ExportSpanFetcherImpl.class); + } + + static final class ExportSpanFetcherImpl + implements DataFetcher> { + private final SpanRequestBuilder requestBuilder; + private final ExportSpanDao exportSpanDao; + + @Inject + ExportSpanFetcherImpl(SpanRequestBuilder requestBuilder, ExportSpanDao exportSpanDao) { + this.requestBuilder = requestBuilder; + this.exportSpanDao = exportSpanDao; + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + Single spanRequest = + this.requestBuilder.build( + contextFromEnvironment(environment), + environment.getArguments(), + SpanAttributes.SPAN_ATTRIBUTES, + LogEventAttributes.LOG_EVENT_ATTRIBUTES); + + return spanRequest + .flatMap(this.exportSpanDao::getSpans) + .toCompletionStage() + .toCompletableFuture(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/SpanFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/SpanFetcher.java new file mode 100644 index 00000000..9079305f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/fetcher/SpanFetcher.java @@ -0,0 +1,42 @@ +package org.hypertrace.core.graphql.span.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.fetcher.InjectableDataFetcher; +import org.hypertrace.core.graphql.span.dao.SpanDao; +import org.hypertrace.core.graphql.span.request.SpanRequestBuilder; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; + +public class SpanFetcher extends InjectableDataFetcher { + + public SpanFetcher() { + super(SpanFetcherImpl.class); + } + + static final class SpanFetcherImpl implements DataFetcher> { + private final SpanRequestBuilder requestBuilder; + private final SpanDao spanDao; + + @Inject + SpanFetcherImpl(SpanRequestBuilder requestBuilder, SpanDao spanDao) { + this.requestBuilder = requestBuilder; + this.spanDao = spanDao; + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + return this.requestBuilder + .build( + contextFromEnvironment(environment), + environment.getArguments(), + environment.getSelectionSet()) + .flatMap(this.spanDao::getSpans) + .toCompletionStage() + .toCompletableFuture(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/DefaultSpanJoinerBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/DefaultSpanJoinerBuilder.java new file mode 100644 index 00000000..4cd98626 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/DefaultSpanJoinerBuilder.java @@ -0,0 +1,223 @@ +package org.hypertrace.core.graphql.span.joiner; + +import static com.google.common.collect.ImmutableList.copyOf; +import static com.google.common.collect.Iterables.concat; +import static org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString.SPAN; +import static org.hypertrace.core.graphql.span.joiner.MultipleSpanJoin.SPANS_KEY; +import static org.hypertrace.core.graphql.span.joiner.SpanJoin.SPAN_KEY; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.id.Identifiable; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.span.dao.SpanDao; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.Span; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +public class DefaultSpanJoinerBuilder implements SpanJoinerBuilder { + + private static final int ZERO_OFFSET = 0; + private final SpanDao spanDao; + private final GraphQlSelectionFinder selectionFinder; + + private final ResultSetRequestBuilder resultSetRequestBuilder; + private final FilterRequestBuilder filterRequestBuilder; + + @Inject + DefaultSpanJoinerBuilder( + SpanDao spanDao, + GraphQlSelectionFinder selectionFinder, + ResultSetRequestBuilder resultSetRequestBuilder, + FilterRequestBuilder filterRequestBuilder) { + this.spanDao = spanDao; + this.selectionFinder = selectionFinder; + this.resultSetRequestBuilder = resultSetRequestBuilder; + this.filterRequestBuilder = filterRequestBuilder; + } + + @Override + public Single build( + GraphQlRequestContext context, + TimeRangeArgument timeRange, + DataFetchingFieldSelectionSet selectionSet, + List pathToSpanJoin) { + return Single.just(new DefaultSpanJoiner(context, timeRange, selectionSet, pathToSpanJoin)); + } + + @AllArgsConstructor + private class DefaultSpanJoiner implements SpanJoiner { + + private final GraphQlRequestContext context; + private final TimeRangeArgument timeRange; + private final DataFetchingFieldSelectionSet selectionSet; + private final List pathToJoin; + + @Override + public Single> joinSpan( + Collection joinSources, + SpanIdGetter spanIdGetter, + Collection filterArguments) { + Function>> idsGetter = + source -> spanIdGetter.getSpanId(source).map(List::of); + return this.joinSpans(joinSources, idsGetter, filterArguments, SPAN_KEY).map(this::reduceMap); + } + + @Override + public Single> joinSpans( + Collection joinSources, + MultipleSpanIdGetter multipleSpanIdGetter, + Collection filterArguments) { + return this.joinSpans( + joinSources, multipleSpanIdGetter::getSpanIds, filterArguments, SPANS_KEY); + } + + private Single> joinSpans( + Collection joinSources, + Function>> idsGetter, + Collection filterArguments, + String joinSpanKey) { + return this.buildSourceToIdsMap(joinSources, idsGetter) + .flatMap( + sourceToSpanIdsMap -> + this.buildSpanRequest(sourceToSpanIdsMap, joinSpanKey, filterArguments) + .flatMap(spanDao::getSpans) + .map(this::buildSpanIdToSpanMap) + .map( + spanIdToSpanMap -> + this.buildSourceToSpanListMultiMap( + sourceToSpanIdsMap, spanIdToSpanMap))); + } + + private Map reduceMap(ListMultimap listMultimap) { + return listMultimap.entries().stream() + .collect( + Collectors.toUnmodifiableMap( + Entry::getKey, Entry::getValue, (first, second) -> first)); + } + + private Single> buildSourceToIdsMap( + Collection joinSources, Function>> idsGetter) { + return Observable.fromIterable(joinSources) + .flatMapSingle(source -> idsGetter.apply(source).map(ids -> Map.entry(source, ids))) + .collect( + ImmutableListMultimap.flatteningToImmutableListMultimap( + Entry::getKey, entry -> entry.getValue().stream())); + } + + private ImmutableListMultimap buildSourceToSpanListMultiMap( + ListMultimap sourceToSpanIdsMultimap, Map spanIdToSpanMap) { + return sourceToSpanIdsMultimap.entries().stream() + .filter(entry -> spanIdToSpanMap.containsKey(entry.getValue())) + .collect( + ImmutableListMultimap.toImmutableListMultimap( + Entry::getKey, entry -> spanIdToSpanMap.get(entry.getValue()))); + } + + private List getSelections(String joinSpanKey) { + List fullPath = copyOf(concat(pathToJoin, List.of(joinSpanKey))); + return selectionFinder + .findSelections(selectionSet, SelectionQuery.builder().selectionPath(fullPath).build()) + .collect(Collectors.toUnmodifiableList()); + } + + private Map buildSpanIdToSpanMap(SpanResultSet resultSet) { + return resultSet.results().stream() + .collect(Collectors.toUnmodifiableMap(Identifiable::id, Function.identity())); + } + + private Single buildSpanRequest( + ListMultimap sourceToSpanIdsMultimap, + String joinSpanKey, + Collection filterArguments) { + Collection spanIds = + sourceToSpanIdsMultimap.values().stream() + .distinct() + .collect(Collectors.toUnmodifiableList()); + List selectedFields = getSelections(joinSpanKey); + return buildSpanIdsFilter(context, spanIds, filterArguments) + .flatMap(filters -> buildSpanRequest(spanIds.size(), filters, selectedFields)); + } + + private Single buildSpanRequest( + int size, + List> filterArguments, + List selectedFields) { + return resultSetRequestBuilder + .build( + context, + SPAN, + size, + ZERO_OFFSET, + timeRange, + Collections.emptyList(), + filterArguments, + selectedFields.stream(), + Optional.empty()) + .map(spanEventsRequest -> new SpanJoinRequest(context, spanEventsRequest)); + } + + private Single>> buildSpanIdsFilter( + GraphQlRequestContext context, + Collection spanIds, + Collection filterArguments) { + return filterRequestBuilder.build( + context, + SPAN, + Stream.concat(filterArguments.stream(), Stream.of(new SpanIdFilter(spanIds))) + .collect(Collectors.toUnmodifiableList())); + } + } + + @Value + @Accessors(fluent = true) + private static class SpanIdFilter implements FilterArgument { + FilterType type = FilterType.ID; + String key = null; + AttributeExpression keyExpression = null; + FilterOperatorType operator = FilterOperatorType.IN; + Collection value; + AttributeScope idType = null; + String idScope = SPAN; + } + + @Value + @Accessors(fluent = true) + private static class SpanJoinRequest implements SpanRequest { + GraphQlRequestContext context; + ResultSetRequest spanEventsRequest; + Collection logEventAttributes = Collections.emptyList(); + boolean fetchTotal = false; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/MultipleSpanJoin.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/MultipleSpanJoin.java new file mode 100644 index 00000000..afa706e7 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/MultipleSpanJoin.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.span.joiner; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import java.util.List; +import org.hypertrace.core.graphql.span.schema.Span; + +public interface MultipleSpanJoin { + String SPANS_KEY = "spans"; + + @GraphQLField + @GraphQLName(SPANS_KEY) + List spans(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoin.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoin.java new file mode 100644 index 00000000..6611267b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoin.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.graphql.span.joiner; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import org.hypertrace.core.graphql.span.schema.Span; + +public interface SpanJoin { + String SPAN_KEY = "span"; + + @GraphQLField + @GraphQLName(SPAN_KEY) + Span span(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoiner.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoiner.java new file mode 100644 index 00000000..3cc8c8c9 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoiner.java @@ -0,0 +1,64 @@ +package org.hypertrace.core.graphql.span.joiner; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.span.schema.Span; + +public interface SpanJoiner { + + /** A NOOP joiner */ + SpanJoiner NO_OP_JOINER = + new SpanJoiner() { + @Override + public Single> joinSpan( + Collection joinSources, + SpanIdGetter spanIdGetter, + Collection filterArguments) { + return Single.just(Collections.emptyMap()); + } + + @Override + public Single> joinSpans( + Collection joinSources, + MultipleSpanIdGetter multipleSpanIdGetter, + Collection filterArguments) { + return Single.just(ArrayListMultimap.create()); + } + }; + + default Single> joinSpan( + Collection joinSources, SpanIdGetter spanIdGetter) { + return joinSpan(joinSources, spanIdGetter, Collections.emptyList()); + } + + default Single> joinSpans( + Collection joinSources, MultipleSpanIdGetter multipleSpanIdGetter) { + return joinSpans(joinSources, multipleSpanIdGetter, Collections.emptyList()); + } + + Single> joinSpan( + Collection joinSources, + SpanIdGetter spanIdGetter, + Collection filterArguments); + + Single> joinSpans( + Collection joinSources, + MultipleSpanIdGetter multipleSpanIdGetter, + Collection filterArguments); + + @FunctionalInterface + interface SpanIdGetter { + Single getSpanId(T source); + } + + @FunctionalInterface + interface MultipleSpanIdGetter { + Single> getSpanIds(T source); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilder.java new file mode 100644 index 00000000..a06ccf45 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilder.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.span.joiner; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface SpanJoinerBuilder { + Single build( + GraphQlRequestContext context, + TimeRangeArgument timeRange, + DataFetchingFieldSelectionSet selectionSet, + List pathToSpanJoin); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerModule.java new file mode 100644 index 00000000..162eb36b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerModule.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.graphql.span.joiner; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.span.dao.SpanDao; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; + +public class SpanJoinerModule extends AbstractModule { + + @Override + protected void configure() { + bind(SpanJoinerBuilder.class).to(DefaultSpanJoinerBuilder.class); + + requireBinding(SpanDao.class); + requireBinding(GraphQlSelectionFinder.class); + requireBinding(ResultSetRequestBuilder.class); + requireBinding(FilterRequestBuilder.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/DefaultSpanRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/DefaultSpanRequestBuilder.java new file mode 100644 index 00000000..6d498f52 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/DefaultSpanRequestBuilder.java @@ -0,0 +1,91 @@ +package org.hypertrace.core.graphql.span.request; + +import static io.reactivex.rxjava3.core.Single.zip; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +class DefaultSpanRequestBuilder implements SpanRequestBuilder { + + private final ResultSetRequestBuilder resultSetRequestBuilder; + private final LogEventAttributeRequestBuilder logEventAttributeRequestBuilder; + private final GraphQlSelectionFinder selectionFinder; + + @Inject + public DefaultSpanRequestBuilder( + ResultSetRequestBuilder resultSetRequestBuilder, + LogEventAttributeRequestBuilder logEventAttributeRequestBuilder, + GraphQlSelectionFinder selectionFinder) { + this.resultSetRequestBuilder = resultSetRequestBuilder; + this.logEventAttributeRequestBuilder = logEventAttributeRequestBuilder; + this.selectionFinder = selectionFinder; + } + + @Override + public Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet) { + boolean fetchTotal = + this.selectionFinder + .findSelections( + selectionSet, SelectionQuery.namedChild(ResultSet.RESULT_SET_TOTAL_NAME)) + .count() + > 0; + + return zip( + resultSetRequestBuilder.build( + context, + HypertraceCoreAttributeScopeString.SPAN, + arguments, + selectionSet, + OrderArgument.class), + logEventAttributeRequestBuilder.buildAttributeRequest(context, selectionSet), + (resultSetRequest, logEventAttributeRequest) -> + new DefaultSpanRequest( + context, resultSetRequest, logEventAttributeRequest, fetchTotal)); + } + + @Override + public Single build( + GraphQlRequestContext context, + Map arguments, + List spanAttributeExpressions, + List logAttributeExpressions) { + + return zip( + resultSetRequestBuilder.build( + context, HypertraceCoreAttributeScopeString.SPAN, arguments, spanAttributeExpressions), + logEventAttributeRequestBuilder.buildAttributeRequest(context, logAttributeExpressions), + (resultSetRequest, logEventAttributeRequest) -> + // This build method is utilized in the exportSpans API, which does not accept the total + // parameter as an argument. Ref {@link ExportSpanResult}. + // So, we explicitly set fetchTotal to false. + new DefaultSpanRequest(context, resultSetRequest, logEventAttributeRequest, false)); + } + + @Value + @Accessors(fluent = true) + private static class DefaultSpanRequest implements SpanRequest { + GraphQlRequestContext context; + ResultSetRequest spanEventsRequest; + Collection logEventAttributes; + boolean fetchTotal; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilder.java new file mode 100644 index 00000000..7956f2ae --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilder.java @@ -0,0 +1,64 @@ +package org.hypertrace.core.graphql.span.request; + +import static org.hypertrace.core.graphql.common.schema.results.ResultSet.RESULT_SET_RESULTS_NAME; +import static org.hypertrace.core.graphql.span.schema.Span.LOG_EVENT_KEY; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +class LogEventAttributeRequestBuilder { + + private final GraphQlSelectionFinder selectionFinder; + private final AttributeRequestBuilder attributeRequestBuilder; + + @Inject + LogEventAttributeRequestBuilder( + GraphQlSelectionFinder selectionFinder, AttributeRequestBuilder attributeRequestBuilder) { + this.selectionFinder = selectionFinder; + this.attributeRequestBuilder = attributeRequestBuilder; + } + + Single> buildAttributeRequest( + GraphQlRequestContext context, DataFetchingFieldSelectionSet selectionSet) { + return attributeRequestBuilder + .buildForAttributeQueryableFields( + context, + HypertraceCoreAttributeScopeString.LOG_EVENT, + getLogEventSelectionFields(selectionSet)) + .collect(Collectors.toUnmodifiableSet()); + } + + Single> buildAttributeRequest( + GraphQlRequestContext context, List attributeExpressions) { + return Observable.fromIterable(attributeExpressions) + .distinct() + .flatMapSingle( + attributeExpression -> + this.attributeRequestBuilder.buildForAttributeExpression( + context, HypertraceCoreAttributeScopeString.LOG_EVENT, attributeExpression)) + .collect(Collectors.toUnmodifiableSet()); + } + + private Stream getLogEventSelectionFields( + DataFetchingFieldSelectionSet selectionSet) { + return this.selectionFinder.findSelections( + selectionSet, + SelectionQuery.builder() + .selectionPath(List.of(RESULT_SET_RESULTS_NAME, LOG_EVENT_KEY, RESULT_SET_RESULTS_NAME)) + .build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequest.java new file mode 100644 index 00000000..3e04e01d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequest.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.span.request; + +import java.util.Collection; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.ContextualRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; + +public interface SpanRequest extends ContextualRequest { + ResultSetRequest spanEventsRequest(); + + Collection logEventAttributes(); + + boolean fetchTotal(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestBuilder.java new file mode 100644 index 00000000..86f107f1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestBuilder.java @@ -0,0 +1,22 @@ +package org.hypertrace.core.graphql.span.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface SpanRequestBuilder { + + Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet); + + Single build( + GraphQlRequestContext context, + Map arguments, + List spanAttributeExpressions, + List logAttributeExpressions); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestModule.java new file mode 100644 index 00000000..a1d5a39d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/request/SpanRequestModule.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.graphql.span.request; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeAssociator; +import org.hypertrace.core.graphql.common.utils.attributes.AttributeScopeStringTranslator; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; + +public class SpanRequestModule extends AbstractModule { + + @Override + protected void configure() { + bind(SpanRequestBuilder.class).to(DefaultSpanRequestBuilder.class); + requireBinding(ArgumentDeserializer.class); + requireBinding(AttributeRequestBuilder.class); + requireBinding(FilterRequestBuilder.class); + requireBinding(AttributeStore.class); + requireBinding(AttributeAssociator.class); + requireBinding(GraphQlSelectionFinder.class); + requireBinding(AttributeScopeStringTranslator.class); + requireBinding(AttributeRequestBuilder.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/ExportSpanResult.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/ExportSpanResult.java new file mode 100644 index 00000000..1d17d272 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/ExportSpanResult.java @@ -0,0 +1,16 @@ +package org.hypertrace.core.graphql.span.schema; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; + +@GraphQLName(ExportSpanResult.TYPE_NAME) +public interface ExportSpanResult { + String TYPE_NAME = "ExportSpanResult"; + String RESULTS_NAME = "result"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULTS_NAME) + String result(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/Span.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/Span.java new file mode 100644 index 00000000..c591e167 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/Span.java @@ -0,0 +1,19 @@ +package org.hypertrace.core.graphql.span.schema; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeQueryable; +import org.hypertrace.core.graphql.common.schema.id.Identifiable; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; + +@GraphQLName(Span.TYPE_NAME) +public interface Span extends AttributeQueryable, Identifiable { + String TYPE_NAME = "Span"; + String LOG_EVENT_KEY = "logEvents"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(LOG_EVENT_KEY) + LogEventResultSet logEvents(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanResultSet.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanResultSet.java new file mode 100644 index 00000000..abf09e26 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanResultSet.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.span.schema; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; + +@GraphQLName(SpanResultSet.TYPE_NAME) +public interface SpanResultSet extends ResultSet { + String TYPE_NAME = "SpanResultSet"; + + @Override + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULT_SET_RESULTS_NAME) + List results(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanSchema.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanSchema.java new file mode 100644 index 00000000..49693ea6 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/main/java/org/hypertrace/core/graphql/span/schema/SpanSchema.java @@ -0,0 +1,41 @@ +package org.hypertrace.core.graphql.span.schema; + +import graphql.annotations.annotationTypes.GraphQLDataFetcher; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.space.SpaceArgument; +import org.hypertrace.core.graphql.span.fetcher.ExportSpanFetcher; +import org.hypertrace.core.graphql.span.fetcher.SpanFetcher; + +public interface SpanSchema { + String SPANS_QUERY_NAME = "spans"; + String EXPORT_SPANS_QUERY_NAME = "exportSpans"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(SPANS_QUERY_NAME) + @GraphQLDataFetcher(SpanFetcher.class) + SpanResultSet spans( + @GraphQLName(TimeRangeArgument.ARGUMENT_NAME) @GraphQLNonNull TimeRangeArgument between, + @GraphQLName(FilterArgument.ARGUMENT_NAME) List filterBy, + @GraphQLName(OrderArgument.ARGUMENT_NAME) List orderBy, + @GraphQLName(LimitArgument.ARGUMENT_NAME) int limit, + @GraphQLName(OffsetArgument.ARGUMENT_NAME) int offset, + @GraphQLName(SpaceArgument.ARGUMENT_NAME) String space); + + @GraphQLField + @GraphQLNonNull + @GraphQLName(EXPORT_SPANS_QUERY_NAME) + @GraphQLDataFetcher(ExportSpanFetcher.class) + ExportSpanResult exportSpans( + @GraphQLName(TimeRangeArgument.ARGUMENT_NAME) @GraphQLNonNull TimeRangeArgument between, + @GraphQLName(FilterArgument.ARGUMENT_NAME) List filterBy, + @GraphQLName(LimitArgument.ARGUMENT_NAME) int limit); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/DaoTestUtil.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/DaoTestUtil.java new file mode 100644 index 00000000..19a9d6f2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/DaoTestUtil.java @@ -0,0 +1,227 @@ +package org.hypertrace.core.graphql.span.dao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeModelMetricAggregationType; +import org.hypertrace.core.graphql.attributes.AttributeModelType; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeScope; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterOperatorType; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterType; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.hypertrace.gateway.service.v1.log.events.LogEvent; +import org.hypertrace.gateway.service.v1.log.events.LogEventsResponse; +import org.hypertrace.gateway.service.v1.span.SpanEvent; +import org.hypertrace.gateway.service.v1.span.SpansResponse; + +class DaoTestUtil { + + @Value + @Accessors(fluent = true) + static class DefaultAttributeRequest implements AttributeRequest { + AttributeAssociation attributeExpressionAssociation; + } + + @Value + @Accessors(fluent = true) + static class DefaultAttributeModel implements AttributeModel { + String id; + String scope; + String key; + String displayName; + AttributeModelType type; + String units; + boolean onlySupportsGrouping; + boolean onlySupportsAggregation; + List supportedMetricAggregationTypes; + boolean groupable; + boolean isCustom; + } + + @Value + @Accessors(fluent = true) + static class NormalizedFilter implements FilterArgument { + FilterType type = FilterType.ATTRIBUTE; + String key = null; + AttributeExpression keyExpression; + FilterOperatorType operator; + Object value; + String idScope = null; + AttributeScope idType = null; + } + + @Value + @Accessors(fluent = true) + static class DefaultSpanRequest implements SpanRequest { + GraphQlRequestContext context; + ResultSetRequest spanEventsRequest; + Collection logEventAttributes; + boolean fetchTotal; + } + + @Value + @Accessors(fluent = true) + static class DefaultResultSetRequest implements ResultSetRequest { + GraphQlRequestContext context; + Collection attributes; + TimeRangeArgument timeRange; + AttributeRequest idAttribute; + int limit; + int offset; + List> orderArguments; + Collection> filterArguments; + Optional spaceId; + } + + @Value + @Accessors(fluent = true) + static class DefaultTimeRange implements TimeRangeArgument { + + @JsonProperty(TIME_RANGE_ARGUMENT_START_TIME) + Instant startTime; + + @JsonProperty(TIME_RANGE_ARGUMENT_END_TIME) + Instant endTime; + } + + static AttributeRequest traceIdAttribute = + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "traceId", + "LOG_EVENT", + "traceId", + "Trace Id", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("traceId"))); + + static AttributeRequest spanIdAttribute = + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "spanId", + "LOG_EVENT", + "spanId", + "Span Id", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("spanId"))); + + static AttributeRequest attributesAttribute = + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "attributes", + "LOG_EVENT", + "attributes", + "Attributes", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("attributes"))); + + static AttributeRequest eventIdAttribute = + new DefaultAttributeRequest( + AttributeAssociation.of( + new DefaultAttributeModel( + "id", + "EVENT", + "id", + "Id", + AttributeModelType.STRING, + "", + false, + false, + Collections.emptyList(), + false, + false), + AttributeExpression.forAttributeKey("id"))); + + static SpansResponse spansResponse = + SpansResponse.newBuilder() + .addSpans( + SpanEvent.newBuilder() + .putAllAttributes(Map.of("id", getValue("span1"), "traceId", getValue("trace1"))) + .build()) + .addSpans( + SpanEvent.newBuilder() + .putAllAttributes(Map.of("id", getValue("span2"), "traceId", getValue("trace1"))) + .build()) + .addSpans( + SpanEvent.newBuilder() + .putAllAttributes(Map.of("id", getValue("span3"), "traceId", getValue("trace1"))) + .build()) + .build(); + + static LogEventsResponse logEventsResponse = + LogEventsResponse.newBuilder() + .addLogEvents( + LogEvent.newBuilder() + .putAllAttributes( + Map.of( + "traceId", + getValue("trace1"), + "attributes", + getValue("event: error"), + "spanId", + getValue("span1")))) + .addLogEvents( + LogEvent.newBuilder() + .putAllAttributes( + Map.of( + "traceId", + getValue("trace1"), + "attributes", + getValue("event: error"), + "spanId", + getValue("span1")))) + .addLogEvents( + LogEvent.newBuilder() + .putAllAttributes( + Map.of( + "traceId", + getValue("trace1"), + "attributes", + getValue("event: error"), + "spanId", + getValue("span2")))) + .build(); + + static org.hypertrace.gateway.service.v1.common.Value getValue(String value) { + return org.hypertrace.gateway.service.v1.common.Value.newBuilder() + .setValueType(ValueType.STRING) + .setString(value) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilderTest.java new file mode 100644 index 00000000..1258ee03 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventRequestBuilderTest.java @@ -0,0 +1,240 @@ +package org.hypertrace.core.graphql.span.dao; + +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.attributesAttribute; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.spanIdAttribute; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.spansResponse; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.traceIdAttribute; +import static org.hypertrace.gateway.service.v1.common.Operator.AND; +import static org.hypertrace.gateway.service.v1.common.Operator.IN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import io.reactivex.rxjava3.core.Single; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.hypertrace.core.graphql.attributes.AttributeModel; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.span.dao.DaoTestUtil.DefaultAttributeRequest; +import org.hypertrace.core.graphql.span.dao.DaoTestUtil.DefaultResultSetRequest; +import org.hypertrace.core.graphql.span.dao.DaoTestUtil.DefaultSpanRequest; +import org.hypertrace.core.graphql.span.dao.DaoTestUtil.DefaultTimeRange; +import org.hypertrace.core.graphql.span.dao.DaoTestUtil.NormalizedFilter; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.gateway.GatewayUtilsModule; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.LiteralConstant; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.common.ValueType; +import org.hypertrace.gateway.service.v1.log.events.LogEventsRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SpanLogEventRequestBuilderTest { + + @Mock private FilterRequestBuilder filterRequestBuilder; + + @Mock private AttributeStore attributeStore; + + @Mock private AttributeRequestBuilder attributeRequestBuilder; + + private SpanLogEventRequestBuilder spanLogEventRequestBuilder; + + @BeforeEach + void beforeEach() { + Injector injector = + Guice.createInjector( + new GatewayUtilsModule(), + new AbstractModule() { + @Override + protected void configure() { + bind(CallCredentials.class).toInstance(mock(CallCredentials.class)); + bind(GraphQlServiceConfig.class).toInstance(mock(GraphQlServiceConfig.class)); + bind(GrpcChannelRegistry.class).toInstance(mock(GrpcChannelRegistry.class)); + } + }); + + Converter>, Filter> filterConverter = + injector.getInstance( + Key.get( + new TypeLiteral< + Converter>, Filter>>() {})); + + Converter, Set> attributeConverter = + injector.getInstance( + Key.get( + new TypeLiteral, Set>>() {})); + + spanLogEventRequestBuilder = + new SpanLogEventRequestBuilder( + attributeConverter, + filterConverter, + filterRequestBuilder, + attributeStore, + attributeRequestBuilder); + + doAnswer( + invocation -> { + Set filterArguments = invocation.getArgument(2, Set.class); + FilterArgument filterArgument = filterArguments.iterator().next(); + return Single.just( + List.of( + AttributeAssociation.of( + spanIdAttribute.attributeExpressionAssociation().attribute(), + new NormalizedFilter( + AttributeExpression.forAttributeKey( + spanIdAttribute.attributeExpressionAssociation().value().key()), + filterArgument.operator(), + filterArgument.value())))); + }) + .when(filterRequestBuilder) + .build(any(), any(), anyCollection()); + + when(attributeStore.getForeignIdAttribute(any(), anyString(), anyString())) + .thenReturn(Single.just(spanIdAttribute.attributeExpressionAssociation().attribute())); + + doAnswer( + invocation -> { + AttributeModel attributeModel = invocation.getArgument(0, AttributeModel.class); + return new DefaultAttributeRequest( + AttributeAssociation.of( + attributeModel, AttributeExpression.forAttributeKey(attributeModel.key()))); + }) + .when(attributeRequestBuilder) + .buildForAttribute(any()); + } + + @Test + void testBuildRequest() { + + long startTime = System.currentTimeMillis(); + long endTime = System.currentTimeMillis() + Duration.ofHours(1).toMillis(); + + Collection logAttributeRequests = + List.of(spanIdAttribute, traceIdAttribute, attributesAttribute); + DefaultResultSetRequest resultSetRequest = + new DefaultResultSetRequest( + null, + List.of(DaoTestUtil.eventIdAttribute), + new DefaultTimeRange(Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)), + DaoTestUtil.eventIdAttribute, + 0, + 0, + List.of(), + Collections.emptyList(), + Optional.empty()); + SpanRequest spanRequest = + new DefaultSpanRequest(null, resultSetRequest, logAttributeRequests, true); + + LogEventsRequest expectedLogEventsRequest = + LogEventsRequest.newBuilder() + .setStartTimeMillis(startTime) + .setEndTimeMillis(endTime) + .setLimit(1000) + .addSelection(buildAliasedSelection("spanId")) + .addSelection(buildAliasedSelection("traceId")) + .addSelection(buildAliasedSelection("attributes")) + .setFilter( + Filter.newBuilder() + .setOperator(AND) + .addChildFilter( + Filter.newBuilder() + .setLhs(buildAliasedSelection("spanId")) + .setOperator(IN) + .setRhs(buildStringList("span1", "span2", "span3")))) + .build(); + assertEquals( + expectedLogEventsRequest, + spanLogEventRequestBuilder.buildLogEventsRequest(spanRequest, spansResponse).blockingGet()); + } + + @Test + void testBuildRequest_addSpanId() { + long startTime = System.currentTimeMillis(); + long endTime = System.currentTimeMillis() + Duration.ofHours(1).toMillis(); + + Collection logAttributeRequests = List.of(traceIdAttribute); + DefaultResultSetRequest resultSetRequest = + new DefaultResultSetRequest( + null, + List.of(DaoTestUtil.eventIdAttribute), + new DefaultTimeRange(Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)), + DaoTestUtil.eventIdAttribute, + 0, + 0, + List.of(), + Collections.emptyList(), + Optional.empty()); + SpanRequest spanRequest = + new DefaultSpanRequest(null, resultSetRequest, logAttributeRequests, true); + + LogEventsRequest expectedLogEventsRequest = + LogEventsRequest.newBuilder() + .setStartTimeMillis(startTime) + .setEndTimeMillis(endTime) + .setLimit(1000) + .addSelection(buildAliasedSelection("spanId")) + .addSelection(buildAliasedSelection("traceId")) + .setFilter( + Filter.newBuilder() + .setOperator(AND) + .addChildFilter( + Filter.newBuilder() + .setLhs(buildAliasedSelection("spanId")) + .setOperator(IN) + .setRhs(buildStringList("span1", "span2", "span3")))) + .build(); + assertEquals( + expectedLogEventsRequest, + spanLogEventRequestBuilder.buildLogEventsRequest(spanRequest, spansResponse).blockingGet()); + } + + Expression buildAliasedSelection(String name) { + return Expression.newBuilder() + .setAttributeExpression( + org.hypertrace.gateway.service.v1.common.AttributeExpression.newBuilder() + .setAttributeId(name) + .setAlias(name)) + .build(); + } + + Expression buildStringList(String... values) { + return Expression.newBuilder() + .setLiteral( + LiteralConstant.newBuilder() + .setValue( + Value.newBuilder() + .setValueType(ValueType.STRING_ARRAY) + .addAllStringArray(List.of(values)))) + .build(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverterTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverterTest.java new file mode 100644 index 00000000..6d8211bc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/dao/SpanLogEventResponseConverterTest.java @@ -0,0 +1,121 @@ +package org.hypertrace.core.graphql.span.dao; + +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.attributesAttribute; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.logEventsResponse; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.spanIdAttribute; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.spansResponse; +import static org.hypertrace.core.graphql.span.dao.DaoTestUtil.traceIdAttribute; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.hypertrace.core.graphql.attributes.AttributeStore; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.gateway.service.v1.common.Value; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SpanLogEventResponseConverterTest { + + @Mock + BiConverter, Map, Map> + attributeMapConverter; + + @Mock AttributeStore attributeStore; + @Mock GraphQlRequestContext requestContext; + @Mock AttributeRequestBuilder attributeRequestBuilder; + + private SpanLogEventResponseConverter spanLogEventResponseConverter; + + @BeforeEach + void beforeEach() { + spanLogEventResponseConverter = + new SpanLogEventResponseConverter( + attributeMapConverter, attributeStore, attributeRequestBuilder); + } + + @Test + void testBuildResponse() { + Collection attributeRequests = + List.of(spanIdAttribute, traceIdAttribute, attributesAttribute); + + when(attributeStore.getForeignIdAttribute(any(), anyString(), anyString())) + .thenReturn(Single.just(spanIdAttribute.attributeExpressionAssociation().attribute())); + when(attributeRequestBuilder.buildForAttribute( + spanIdAttribute.attributeExpressionAssociation().attribute())) + .thenReturn(spanIdAttribute); + + doAnswer( + invocation -> { + Map map = invocation.getArgument(1, Map.class); + return Single.just( + map.entrySet().stream() + .collect( + Collectors.toMap( + valueEntry -> + AttributeExpression.forAttributeKey(valueEntry.getKey()), + valueEntry -> valueEntry.getValue().getString()))); + }) + .when(attributeMapConverter) + .convert(anyCollection(), anyMap()); + + SpanLogEventsResponse response = + spanLogEventResponseConverter + .buildResponse(requestContext, attributeRequests, spansResponse, logEventsResponse) + .blockingGet(); + + assertEquals(spansResponse, response.spansResponse()); + assertEquals(Set.of("span1", "span2"), response.spanIdToLogEvents().keySet()); + } + + @Test + void testBuildResponse_spanIdNotRequested() { + Collection attributeRequests = List.of(traceIdAttribute, attributesAttribute); + + when(attributeStore.getForeignIdAttribute(any(), anyString(), anyString())) + .thenReturn(Single.just(spanIdAttribute.attributeExpressionAssociation().attribute())); + when(attributeRequestBuilder.buildForAttribute( + spanIdAttribute.attributeExpressionAssociation().attribute())) + .thenReturn(spanIdAttribute); + + doAnswer( + invocation -> { + Map map = invocation.getArgument(1, Map.class); + return Single.just( + map.entrySet().stream() + .collect( + Collectors.toMap( + valueEntry -> + AttributeExpression.forAttributeKey(valueEntry.getKey()), + valueEntry -> valueEntry.getValue().getString()))); + }) + .when(attributeMapConverter) + .convert(anyCollection(), anyMap()); + + SpanLogEventsResponse response = + spanLogEventResponseConverter + .buildResponse(requestContext, attributeRequests, spansResponse, logEventsResponse) + .blockingGet(); + + assertEquals(spansResponse, response.spansResponse()); + assertEquals(Set.of("span1", "span2"), response.spanIdToLogEvents().keySet()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/export/ExportSpanTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/export/ExportSpanTest.java new file mode 100644 index 00000000..884facdc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/export/ExportSpanTest.java @@ -0,0 +1,314 @@ +package org.hypertrace.core.graphql.span.export; + +import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.trace.v1.Span.SpanKind; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.log.event.schema.LogEvent; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.core.graphql.span.export.ExportSpan.Builder; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.LogEventAttributes; +import org.hypertrace.core.graphql.span.export.ExportSpanConstants.SpanAttributes; +import org.hypertrace.core.graphql.span.schema.Span; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ExportSpanTest { + + @Value + @Accessors(fluent = true) + private static class ConvertedSpan implements Span { + String id; + Map attributeValues; + Map> spanIdToLogEvents; + + ConvertedSpan( + String id, + Map attributeMap, + Map> spanIdToLogEvents) { + this.id = id; + this.spanIdToLogEvents = spanIdToLogEvents; + this.attributeValues = + attributeMap.entrySet().stream() + .collect( + Collectors.toUnmodifiableMap( + entry -> AttributeExpression.forAttributeKey(entry.getKey()), + Entry::getValue)); + } + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + + @Override + public LogEventResultSet logEvents() { + List list = spanIdToLogEvents.getOrDefault(id, Collections.emptyList()); + return new ConvertedLogEventResultSet(list, list.size(), list.size()); + } + } + + @Value + @Accessors(fluent = true) + private static class ConvertedLogEventResultSet implements LogEventResultSet { + List results; + long total; + long count; + } + + @Value + @Accessors(fluent = true) + private static class ConvertedLogEvent implements LogEvent { + ConvertedLogEvent(Map attributeMap) { + this.attributeValues = + attributeMap.entrySet().stream() + .collect( + Collectors.toUnmodifiableMap( + entry -> AttributeExpression.forAttributeKey(entry.getKey()), + Entry::getValue)); + } + + Map attributeValues; + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + } + + @Test + public void testIncludeAllFields() { + Span span = + new ConvertedSpan( + "40c4edcea5a17fea", + Map.of( + SpanAttributes.ID, "40c4edcea5a17fea", + SpanAttributes.PARENT_SPAN_ID, "4dfeb104f6ed73bf", + SpanAttributes.TRACE_ID, "00000000000000004dfeb104f6ed73bf", + SpanAttributes.SERVICE_NAME, "frontend", + SpanAttributes.NAME, "HTTP GET: /customer", + SpanAttributes.START_TIME, 1620743557759L, + SpanAttributes.END_TIME, 1620743558040L, + SpanAttributes.TAGS, + Map.of( + "http.status_code", "200", + "component", "net/http", + "span.kind", "server", + "sampler.type", "const", + "sampler.param", "true", + "http.url", "/dispatch?customer=123&nonse=0.04728538603377741", + "servicename", "frontend", + "http.method", "GET", + "status.code", "0")), + Map.of()); + + ExportSpan exportSpan = new Builder(span).build(); + + // check resource + Assertions.assertEquals(1, exportSpan.resourceSpans().getResource().getAttributesCount()); + + Assertions.assertEquals( + "service.name", exportSpan.resourceSpans().getResource().getAttributes(0).getKey()); + Assertions.assertEquals( + "frontend", + exportSpan.resourceSpans().getResource().getAttributes(0).getValue().getStringValue()); + + // check for span count, start time, end time + Assertions.assertEquals(1, exportSpan.resourceSpans().getInstrumentationLibrarySpansCount()); + + Assertions.assertEquals( + 1620743557759L * 1_000_000L, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getStartTimeUnixNano()); + + Assertions.assertEquals( + 1620743558040L * 1_000_000L, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getEndTimeUnixNano()); + + // two attributes are excluded + Assertions.assertEquals( + 7, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getAttributesCount()); + + // test for span.kind + Assertions.assertEquals( + SpanKind.SPAN_KIND_SERVER, + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getKind()); + + // test IDs + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("40c4edcea5a17fea")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getSpanId()); + + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("4dfeb104f6ed73bf")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getParentSpanId()); + + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("00000000000000004dfeb104f6ed73bf")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getTraceId()); + } + + @Test + public void testWithMissingFields() { + Span span = + new ConvertedSpan( + "40c4edcea5a17fea", + Map.of( + SpanAttributes.ID, + "40c4edcea5a17fea", + SpanAttributes.TRACE_ID, + "00000000000000004dfeb104f6ed73bf", + SpanAttributes.START_TIME, + 1620743557759L, + SpanAttributes.END_TIME, + 1620743558040L), + Map.of()); + + ExportSpan exportSpan = new Builder(span).build(); + + // check resource + Assertions.assertEquals(0, exportSpan.resourceSpans().getResource().getAttributesCount()); + + // check for span count, start time, end time + Assertions.assertEquals(1, exportSpan.resourceSpans().getInstrumentationLibrarySpansCount()); + + Assertions.assertEquals( + 1620743557759L * 1_000_000L, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getStartTimeUnixNano()); + + Assertions.assertEquals( + 1620743558040L * 1_000_000L, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getEndTimeUnixNano()); + + // two attributes are excluded + Assertions.assertEquals( + 0, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getAttributesCount()); + + // test for span.kind + Assertions.assertEquals( + SpanKind.SPAN_KIND_UNSPECIFIED, + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getKind()); + + // test IDs + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("40c4edcea5a17fea")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getSpanId()); + + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getParentSpanId()); + + Assertions.assertEquals( + ByteString.copyFrom(BaseEncoding.base64().decode("00000000000000004dfeb104f6ed73bf")), + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getTraceId()); + } + + @Test + public void testForLogEventsFields() { + List logEventsUnderTestSpanId = + List.of( + new ConvertedLogEvent( + Map.of( + LogEventAttributes.TIMESTAMP, + "2021-05-25T16:26:00.310958Z", + LogEventAttributes.ATTRIBUTES, + Map.of("level", "warn", "message", "redis timeout")))); + + List logEvents = + List.of( + new ConvertedLogEvent( + Map.of( + LogEventAttributes.TIMESTAMP, + "2021-05-25T14:57:57.187Z", + LogEventAttributes.ATTRIBUTES, + Map.of("level", "info", "message", "request successful")))); + + Span span = + new ConvertedSpan( + "40c4edcea5a17fea", + Map.of( + SpanAttributes.ID, + "40c4edcea5a17fea", + SpanAttributes.PARENT_SPAN_ID, + "4dfeb104f6ed73bf", + SpanAttributes.TRACE_ID, + "00000000000000004dfeb104f6ed73bf", + SpanAttributes.START_TIME, + 1620743557759L, + SpanAttributes.END_TIME, + 1620743558040L), + Map.of( + "40c4edcea5a17fea", logEventsUnderTestSpanId, + "40c4edcea5a17feb", logEvents)); + + ExportSpan exportSpan = new Builder(span).build(); + + // check for logs + Assertions.assertEquals( + 1, + exportSpan.resourceSpans().getInstrumentationLibrarySpans(0).getSpans(0).getEventsCount()); + + Assertions.assertEquals( + 1621959960310958000L, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getEvents(0) + .getTimeUnixNano()); + + Assertions.assertEquals( + 2, + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getEvents(0) + .getAttributesCount()); + + Assertions.assertTrue( + exportSpan + .resourceSpans() + .getInstrumentationLibrarySpans(0) + .getSpans(0) + .getEvents(0) + .getAttributesList() + .stream() + .anyMatch( + attribute -> + attribute.getKey().equals("level") + && attribute.getValue().getStringValue().equals("warn"))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilderTest.java new file mode 100644 index 00000000..7242cf9a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/joiner/SpanJoinerBuilderTest.java @@ -0,0 +1,223 @@ +package org.hypertrace.core.graphql.span.joiner; + +import static java.util.Collections.emptyList; +import static java.util.Map.entry; +import static org.hypertrace.core.graphql.atttributes.scopes.HypertraceCoreAttributeScopeString.SPAN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Single; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.FilterRequestBuilder; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.log.event.schema.LogEventResultSet; +import org.hypertrace.core.graphql.span.dao.SpanDao; +import org.hypertrace.core.graphql.span.joiner.SpanJoiner.MultipleSpanIdGetter; +import org.hypertrace.core.graphql.span.joiner.SpanJoiner.SpanIdGetter; +import org.hypertrace.core.graphql.span.request.SpanRequest; +import org.hypertrace.core.graphql.span.schema.Span; +import org.hypertrace.core.graphql.span.schema.SpanResultSet; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SpanJoinerBuilderTest { + + private static final String SPAN_ID1 = "spanId1"; + private static final String SPAN_ID2 = "spanId2"; + private static final String SPAN_ID3 = "spanId3"; + private static final String SPAN_ID4 = "spanId4"; + + @Mock private SpanDao mockSpanDao; + @Mock private GraphQlSelectionFinder mockSelectionFinder; + @Mock private ResultSetRequestBuilder mockResultSetRequestBuilder; + @Mock private FilterRequestBuilder mockFilterRequestBuilder; + @Mock private DataFetchingFieldSelectionSet mockSelectionSet; + @Mock private GraphQlRequestContext mockRequestContext; + @Mock private ResultSetRequest mockResultSetRequest; + @Mock private AttributeAssociation mockFilter; + @Mock private TimeRangeArgument mockTimeRangeArgument; + + private SpanJoinerBuilder spanJoinerBuilder; + + @BeforeEach + void setup() { + spanJoinerBuilder = + new DefaultSpanJoinerBuilder( + mockSpanDao, + mockSelectionFinder, + mockResultSetRequestBuilder, + mockFilterRequestBuilder); + } + + @Test + void fetchSpans() { + Span span1 = new TestSpan(SPAN_ID1); + Span span2 = new TestSpan(SPAN_ID2); + TestJoinSource joinSource1 = new TestJoinSource(SPAN_ID1); + TestJoinSource joinSource2 = new TestJoinSource(SPAN_ID2); + Map expected = + Map.ofEntries(entry(joinSource1, span1), entry(joinSource2, span2)); + List joinSources = List.of(joinSource1, joinSource2); + when(mockSelectionFinder.findSelections( + mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("pathToSpan", "span")).build())) + .thenReturn(Stream.of(mock(SelectedField.class), mock(SelectedField.class))); + when(mockFilterRequestBuilder.build(eq(mockRequestContext), eq(SPAN), anyList())) + .thenReturn(Single.just(List.of(mockFilter))); + + when(mockResultSetRequestBuilder.build( + eq(mockRequestContext), + eq(SPAN), + eq(2), + eq(0), + eq(mockTimeRangeArgument), + eq(emptyList()), + eq(List.of(mockFilter)), + any(Stream.class), + eq(Optional.empty()))) + .thenReturn(Single.just(mockResultSetRequest)); + mockResult(List.of(span1, span2)); + SpanJoiner joiner = + this.spanJoinerBuilder + .build( + this.mockRequestContext, + this.mockTimeRangeArgument, + this.mockSelectionSet, + List.of("pathToSpan")) + .blockingGet(); + assertEquals( + expected, + joiner + .joinSpan(joinSources, new TestJoinSourceIdGetter(), Collections.emptyList()) + .blockingGet()); + } + + @Test + void fetchMultipleSpans() { + Span span1 = new TestSpan(SPAN_ID1); + Span span2 = new TestSpan(SPAN_ID2); + TestMultipleJoinSource joinSource1 = new TestMultipleJoinSource(List.of(SPAN_ID1, SPAN_ID2)); + TestMultipleJoinSource joinSource2 = new TestMultipleJoinSource(List.of(SPAN_ID3, SPAN_ID4)); + ListMultimap expected = ArrayListMultimap.create(); + expected.put(joinSource1, span1); + expected.put(joinSource1, span2); + List joinSources = List.of(joinSource1, joinSource2); + when(mockSelectionFinder.findSelections( + mockSelectionSet, + SelectionQuery.builder().selectionPath(List.of("pathToSpans", "spans")).build())) + .thenReturn(Stream.of(mock(SelectedField.class), mock(SelectedField.class))); + when(mockFilterRequestBuilder.build(eq(mockRequestContext), eq(SPAN), anyList())) + .thenReturn(Single.just(List.of(mockFilter))); + + when(mockResultSetRequestBuilder.build( + eq(mockRequestContext), + eq(SPAN), + eq(4), + eq(0), + eq(mockTimeRangeArgument), + eq(emptyList()), + eq(List.of(mockFilter)), + any(Stream.class), + eq(Optional.empty()))) + .thenReturn(Single.just(mockResultSetRequest)); + + mockResult(List.of(span1, span2)); + SpanJoiner joiner = + this.spanJoinerBuilder + .build( + this.mockRequestContext, + this.mockTimeRangeArgument, + this.mockSelectionSet, + List.of("pathToSpans")) + .blockingGet(); + assertEquals( + expected, + joiner + .joinSpans(joinSources, new TestMultipleJoinSourceIdGetter(), Collections.emptyList()) + .blockingGet()); + } + + private void mockResult(List spans) { + when(mockSpanDao.getSpans(any(SpanRequest.class))) + .thenReturn(Single.just(new TestSpanResultSet(spans))); + } + + @Value + private static class TestJoinSource { + String spanId; + } + + private static class TestJoinSourceIdGetter implements SpanIdGetter { + @Override + public Single getSpanId(TestJoinSource source) { + if (source.getSpanId() == null || source.getSpanId().isEmpty()) { + return Single.error(new IllegalArgumentException("Empty spanId")); + } + return Single.just(source.getSpanId()); + } + } + + @Value + @Accessors(fluent = true) + private static class TestSpanResultSet implements SpanResultSet { + List results; + long count = 0; + long total = 0; + } + + @Value + @Accessors(fluent = true) + private static class TestSpan implements Span { + String id; + + @Override + public Object attribute(AttributeExpression expression) { + return null; + } + + @Override + public LogEventResultSet logEvents() { + return null; + } + } + + @Value + private static class TestMultipleJoinSource { + List spanIds; + } + + private static class TestMultipleJoinSourceIdGetter + implements MultipleSpanIdGetter { + @Override + public Single> getSpanIds(TestMultipleJoinSource source) { + return Single.just(source.getSpanIds()); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilderTest.java b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilderTest.java new file mode 100644 index 00000000..d5efcca0 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-span-schema/src/test/java/org/hypertrace/core/graphql/span/request/LogEventAttributeRequestBuilderTest.java @@ -0,0 +1,59 @@ +package org.hypertrace.core.graphql.span.request; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; +import io.reactivex.rxjava3.core.Observable; +import java.util.Set; +import java.util.stream.Stream; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.AttributeRequestBuilder; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LogEventAttributeRequestBuilderTest { + + @Mock GraphQlRequestContext mockContext; + @Mock GraphQlSelectionFinder mockSelectionFinder; + @Mock AttributeRequestBuilder mockAttributeRequestBuilder; + @Mock DataFetchingFieldSelectionSet mockSelectionSet; + @Mock Stream mockAttributeQueryableStream; + @Mock AttributeRequest mockFooAttributeRequest; + @Mock AttributeRequest mockBarAttributeRequest; + + private LogEventAttributeRequestBuilder requestBuilder; + + @BeforeEach + void beforeEach() { + this.requestBuilder = + new LogEventAttributeRequestBuilder( + this.mockSelectionFinder, this.mockAttributeRequestBuilder); + } + + @Test + void canBuildRequest() { + when(this.mockSelectionFinder.findSelections(eq(this.mockSelectionSet), any())) + .thenReturn(mockAttributeQueryableStream); + when(this.mockAttributeRequestBuilder.buildForAttributeQueryableFields( + any(), any(), eq(this.mockAttributeQueryableStream))) + .thenReturn(Observable.just(this.mockFooAttributeRequest, this.mockBarAttributeRequest)); + + Set request = + this.requestBuilder + .buildAttributeRequest(this.mockContext, this.mockSelectionSet) + .blockingGet(); + + Assertions.assertEquals( + Set.of(this.mockBarAttributeRequest, this.mockFooAttributeRequest), request); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-spi/build.gradle.kts new file mode 100644 index 00000000..543f129a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.graphql.java) + api(localLibs.graphql.annotations) + api(commonLibs.jsr305) +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-spi/gradle.lockfile new file mode 100644 index 00000000..afdc19ec --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/gradle.lockfile @@ -0,0 +1,16 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlEndpointConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlEndpointConfig.java new file mode 100644 index 00000000..b92be153 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlEndpointConfig.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.spi.config; + +/** This specifies the configuration of one graphql endpoint in the server */ +public interface GraphQlEndpointConfig { + + boolean isIntrospectionAllowed(); + + boolean isCorsEnabled(); + + java.time.Duration getTimeout(); + + String getUrlPath(); + + int getMaxRequestThreads(); + + int getMaxIoThreads(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlServiceConfig.java b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlServiceConfig.java new file mode 100644 index 00000000..11b011d6 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/config/GraphQlServiceConfig.java @@ -0,0 +1,27 @@ +package org.hypertrace.core.graphql.spi.config; + +import java.time.Duration; +import java.util.Optional; + +public interface GraphQlServiceConfig { + + int getServicePort(); + + String getServiceName(); + + Optional getDefaultTenantId(); + + String getAttributeServiceHost(); + + int getAttributeServicePort(); + + Duration getAttributeServiceTimeout(); + + String getGatewayServiceHost(); + + int getGatewayServicePort(); + + Duration getGatewayServiceTimeout(); + + int getGatewayServiceMaxInboundMessageSize(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/lifecycle/GraphQlServiceLifecycle.java b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/lifecycle/GraphQlServiceLifecycle.java new file mode 100644 index 00000000..e6963975 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/lifecycle/GraphQlServiceLifecycle.java @@ -0,0 +1,8 @@ +package org.hypertrace.core.graphql.spi.lifecycle; + +import java.util.concurrent.CompletionStage; + +public interface GraphQlServiceLifecycle { + + CompletionStage shutdownCompletion(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/schema/GraphQlSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/schema/GraphQlSchemaFragment.java new file mode 100644 index 00000000..e8070ba2 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-spi/src/main/java/org/hypertrace/core/graphql/spi/schema/GraphQlSchemaFragment.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.graphql.spi.schema; + +import graphql.annotations.processor.typeFunctions.TypeFunction; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface GraphQlSchemaFragment { + + String fragmentName(); + + @Nullable + default Class annotatedQueryClass() { + return null; + } + + @Nullable + default Class annotatedMutationClass() { + return null; + } + + @Nonnull + default List typeFunctions() { + return List.of(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/build.gradle.kts b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/build.gradle.kts new file mode 100644 index 00000000..580afab1 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `java-library` +} + +dependencies { + api(commonLibs.guice) + api(commonLibs.graphql.java) + api(projects.hypertraceCoreGraphqlSpi) + api(localLibs.graphql.annotations) + + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + + compileOnly(projects.hypertraceCoreGraphqlAttributeScopeConstants) + + implementation(commonLibs.slf4j2.api) + implementation(commonLibs.rxjava3) + implementation(commonLibs.hypertrace.gatewayservice.api) + implementation(commonLibs.protobuf.javautil) + + implementation(projects.hypertraceCoreGraphqlContext) + implementation(projects.hypertraceCoreGraphqlGrpcUtils) + implementation(projects.hypertraceCoreGraphqlCommonSchema) + implementation(projects.hypertraceCoreGraphqlAttributeStore) + implementation(projects.hypertraceCoreGraphqlDeserialization) + implementation(projects.hypertraceCoreGraphqlRequestTransformation) + implementation(projects.hypertraceCoreGraphqlSchemaUtils) +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/gradle.lockfile b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/gradle.lockfile new file mode 100644 index 00000000..c1939053 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/gradle.lockfile @@ -0,0 +1,64 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +aopalliance:aopalliance:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.auth0:java-jwt:4.4.0=runtimeClasspath,testRuntimeClasspath +com.auth0:jwks-rsa:0.22.0=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1=runtimeClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.16.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.android:annotations:4.1.1.4=runtimeClasspath,testRuntimeClasspath +com.google.api.grpc:proto-google-common-protos:2.22.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.8.9=compileClasspath,testCompileClasspath +com.google.errorprone:error_prone_annotations:2.20.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.inject:guice:6.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:2.8=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java-util:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.protobuf:protobuf-java:3.24.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-kickstart:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java-kickstart:graphql-java-servlet:14.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java-extended-scalars:17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:graphql-java:19.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.graphql-java:java-dataloader:3.2.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.github.graphql-java:graphql-java-annotations:9.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-bom:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-context:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-core:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-inprocess:1.60.0=runtimeClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf-lite:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-protobuf:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-stub:1.60.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-util:1.60.0=runtimeClasspath,testRuntimeClasspath +io.netty:netty-bom:4.1.108.Final=runtimeClasspath,testRuntimeClasspath +io.perfmark:perfmark-api:0.26.0=runtimeClasspath,testRuntimeClasspath +io.reactivex.rxjava3:rxjava:3.1.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.annotation:javax.annotation-api:1.3.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.inject:javax.inject:1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:4.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.validation:validation-api:1.1.0.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +javax.websocket:javax.websocket-api:1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.checkerframework:checker-qual:3.33.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.mojo:animal-sniffer-annotations:1.23=runtimeClasspath,testRuntimeClasspath +org.hypertrace.bom:hypertrace-bom:0.3.23=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:attribute-service-api:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.attribute.service:caching-attribute-service-client:0.14.35=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-rx-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-client-utils:0.13.4=runtimeClasspath,testRuntimeClasspath +org.hypertrace.core.grpcutils:grpc-context-utils:0.13.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.core.kafkastreams.framework:kafka-bom:0.4.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hypertrace.gateway.service:gateway-service-api:0.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.7=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty= diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaFragment.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaFragment.java new file mode 100644 index 00000000..72161b21 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaFragment.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.trace; + +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; +import org.hypertrace.core.graphql.trace.schema.TraceSchema; + +public class TraceSchemaFragment implements GraphQlSchemaFragment { + + @Override + public String fragmentName() { + return "Trace schema"; + } + + @Override + public Class annotatedQueryClass() { + return TraceSchema.class; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaModule.java new file mode 100644 index 00000000..0025503f --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/TraceSchemaModule.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.graphql.trace; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.spi.schema.GraphQlSchemaFragment; +import org.hypertrace.core.graphql.trace.dao.TraceDaoModule; +import org.hypertrace.core.graphql.trace.deserialization.TraceDeserializationModule; +import org.hypertrace.core.graphql.trace.request.TraceRequestModule; + +public class TraceSchemaModule extends AbstractModule { + @Override + protected void configure() { + Multibinder.newSetBinder(binder(), GraphQlSchemaFragment.class) + .addBinding() + .to(TraceSchemaFragment.class); + + install(new TraceRequestModule()); + install(new TraceDeserializationModule()); + install(new TraceDaoModule()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceConverter.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceConverter.java new file mode 100644 index 00000000..0a637e46 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceConverter.java @@ -0,0 +1,72 @@ +package org.hypertrace.core.graphql.trace.dao; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.trace.schema.Trace; +import org.hypertrace.core.graphql.trace.schema.TraceResultSet; +import org.hypertrace.gateway.service.v1.common.Value; +import org.hypertrace.gateway.service.v1.trace.TracesResponse; + +public class GatewayServiceTraceConverter { + private final BiConverter< + Collection, Map, Map> + attributeMapConverter; + + @Inject + GatewayServiceTraceConverter( + BiConverter< + Collection, Map, Map> + attributeMapConverter) { + this.attributeMapConverter = attributeMapConverter; + } + + public Single convert(ResultSetRequest request, TracesResponse response) { + int total = response.hasTotal() ? response.getTotal() : 0; + return Observable.fromIterable(response.getTracesList()) + .flatMapSingle(trace -> this.convert(request, trace)) + .toList() + .map(traces -> new ConvertedTraceResultSet(traces, total, traces.size())); + } + + private Single convert( + ResultSetRequest request, org.hypertrace.gateway.service.v1.trace.Trace trace) { + return this.attributeMapConverter + .convert(request.attributes(), trace.getAttributesMap()) + .map( + attrMap -> + new ConvertedTrace( + attrMap + .get(request.idAttribute().attributeExpressionAssociation().value()) + .toString(), + attrMap)); + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedTrace implements Trace { + String id; + Map attributeValues; + + @Override + public Object attribute(AttributeExpression attributeExpression) { + return this.attributeValues.get(attributeExpression); + } + } + + @lombok.Value + @Accessors(fluent = true) + private static class ConvertedTraceResultSet implements TraceResultSet { + List results; + long total; + long count; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceDao.java new file mode 100644 index 00000000..e75c472b --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceDao.java @@ -0,0 +1,75 @@ +package org.hypertrace.core.graphql.trace.dao; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import io.grpc.CallCredentials; +import io.reactivex.rxjava3.core.Single; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.request.transformation.RequestTransformer; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.trace.request.TraceRequest; +import org.hypertrace.core.graphql.trace.schema.TraceResultSet; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.GatewayServiceGrpc; +import org.hypertrace.gateway.service.GatewayServiceGrpc.GatewayServiceFutureStub; +import org.hypertrace.gateway.service.v1.trace.TracesRequest; +import org.hypertrace.gateway.service.v1.trace.TracesResponse; + +@Singleton +class GatewayServiceTraceDao implements TraceDao { + + private final GatewayServiceFutureStub gatewayServiceStub; + private final GrpcContextBuilder grpcContextBuilder; + private final GatewayServiceTraceRequestBuilder requestBuilder; + private final GatewayServiceTraceConverter traceConverter; + private final GraphQlServiceConfig serviceConfig; + private final RequestTransformer requestTransformer; + + @Inject + GatewayServiceTraceDao( + GraphQlServiceConfig serviceConfig, + CallCredentials credentials, + GrpcContextBuilder grpcContextBuilder, + GrpcChannelRegistry channelRegistry, + GatewayServiceTraceRequestBuilder requestBuilder, + GatewayServiceTraceConverter traceConverter, + RequestTransformer requestTransformer) { + this.grpcContextBuilder = grpcContextBuilder; + this.requestBuilder = requestBuilder; + this.traceConverter = traceConverter; + this.serviceConfig = serviceConfig; + this.requestTransformer = requestTransformer; + + this.gatewayServiceStub = + GatewayServiceGrpc.newFutureStub( + channelRegistry.forAddress( + serviceConfig.getGatewayServiceHost(), serviceConfig.getGatewayServicePort())) + .withCallCredentials(credentials); + } + + @Override + public Single getTraces(TraceRequest request) { + return this.requestTransformer + .transform(request) + .flatMap(this.requestBuilder::buildRequest) + .flatMap(serverRequest -> this.makeRequest(request.context(), serverRequest)) + .flatMap( + serverResponse -> + this.traceConverter.convert(request.resultSetRequest(), serverResponse)); + } + + private Single makeRequest(GraphQlRequestContext context, TracesRequest request) { + return Single.fromFuture( + this.grpcContextBuilder + .build(context) + .call( + () -> + this.gatewayServiceStub + .withDeadlineAfter( + serviceConfig.getGatewayServiceTimeout().toMillis(), MILLISECONDS) + .getTraces(request))); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceRequestBuilder.java new file mode 100644 index 00000000..1cb37645 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/GatewayServiceTraceRequestBuilder.java @@ -0,0 +1,60 @@ +package org.hypertrace.core.graphql.trace.dao; + +import static io.reactivex.rxjava3.core.Single.zip; + +import io.reactivex.rxjava3.core.Single; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.trace.request.TraceRequest; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.trace.TracesRequest; + +class GatewayServiceTraceRequestBuilder { + + private final Converter>, Filter> filterConverter; + private final Converter>, List> + orderConverter; + private final Converter, Set> selectionConverter; + + @Inject + GatewayServiceTraceRequestBuilder( + Converter>, Filter> filterConverter, + Converter>, List> orderConverter, + Converter, Set> selectionConverter) { + this.filterConverter = filterConverter; + this.orderConverter = orderConverter; + this.selectionConverter = selectionConverter; + } + + Single buildRequest(TraceRequest request) { + + return zip( + this.selectionConverter.convert(request.resultSetRequest().attributes()), + this.orderConverter.convert(request.resultSetRequest().orderArguments()), + this.filterConverter.convert(request.resultSetRequest().filterArguments()), + (selections, orderBys, filters) -> + TracesRequest.newBuilder() + .setScope(request.traceType().getScopeString()) + .setStartTimeMillis( + request.resultSetRequest().timeRange().startTime().toEpochMilli()) + .setEndTimeMillis(request.resultSetRequest().timeRange().endTime().toEpochMilli()) + .addAllSelection(selections) + .addAllOrderBy(orderBys) + .setLimit(request.resultSetRequest().limit()) + .setOffset(request.resultSetRequest().offset()) + .setFilter(filters) + .setSpaceId( + request.resultSetRequest().spaceId().orElse("")) // String proto default value + .setFetchTotal(request.fetchTotal()) + .build()); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDao.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDao.java new file mode 100644 index 00000000..6ba80016 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDao.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.trace.dao; + +import io.reactivex.rxjava3.core.Single; +import org.hypertrace.core.graphql.trace.request.TraceRequest; +import org.hypertrace.core.graphql.trace.schema.TraceResultSet; + +public interface TraceDao { + + Single getTraces(TraceRequest request); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDaoModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDaoModule.java new file mode 100644 index 00000000..77ca9524 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/dao/TraceDaoModule.java @@ -0,0 +1,60 @@ +package org.hypertrace.core.graphql.trace.dao; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import io.grpc.CallCredentials; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.hypertrace.core.graphql.common.request.AttributeAssociation; +import org.hypertrace.core.graphql.common.request.AttributeRequest; +import org.hypertrace.core.graphql.common.schema.attributes.arguments.AttributeExpression; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.utils.BiConverter; +import org.hypertrace.core.graphql.common.utils.Converter; +import org.hypertrace.core.graphql.request.transformation.RequestTransformer; +import org.hypertrace.core.graphql.spi.config.GraphQlServiceConfig; +import org.hypertrace.core.graphql.utils.grpc.GrpcChannelRegistry; +import org.hypertrace.core.graphql.utils.grpc.GrpcContextBuilder; +import org.hypertrace.gateway.service.v1.common.Expression; +import org.hypertrace.gateway.service.v1.common.Filter; +import org.hypertrace.gateway.service.v1.common.OrderByExpression; +import org.hypertrace.gateway.service.v1.common.Value; + +public class TraceDaoModule extends AbstractModule { + + @Override + protected void configure() { + bind(TraceDao.class).to(GatewayServiceTraceDao.class); + requireBinding(CallCredentials.class); + requireBinding(GraphQlServiceConfig.class); + requireBinding(GrpcContextBuilder.class); + requireBinding(GrpcChannelRegistry.class); + requireBinding(RequestTransformer.class); + + requireBinding( + Key.get(new TypeLiteral, Set>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter< + List>, List>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + Converter>, Filter>>() {})); + + requireBinding( + Key.get( + new TypeLiteral< + BiConverter< + Collection, + Map, + Map>>() {})); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/deserialization/TraceDeserializationModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/deserialization/TraceDeserializationModule.java new file mode 100644 index 00000000..0c171423 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/deserialization/TraceDeserializationModule.java @@ -0,0 +1,21 @@ +package org.hypertrace.core.graphql.trace.deserialization; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializationConfig; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceTypeArgument; + +public class TraceDeserializationModule extends AbstractModule { + @Override + protected void configure() { + + Multibinder deserializationConfigMultibinder = + Multibinder.newSetBinder(binder(), ArgumentDeserializationConfig.class); + + deserializationConfigMultibinder + .addBinding() + .toInstance( + ArgumentDeserializationConfig.forPrimitive( + TraceTypeArgument.ARGUMENT_NAME, TraceTypeArgument.class)); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/fetcher/TraceFetcher.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/fetcher/TraceFetcher.java new file mode 100644 index 00000000..8f9f3f02 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/fetcher/TraceFetcher.java @@ -0,0 +1,42 @@ +package org.hypertrace.core.graphql.trace.fetcher; + +import static org.hypertrace.core.graphql.context.GraphQlRequestContext.contextFromEnvironment; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.hypertrace.core.graphql.common.fetcher.InjectableDataFetcher; +import org.hypertrace.core.graphql.trace.dao.TraceDao; +import org.hypertrace.core.graphql.trace.request.TraceRequestBuilder; +import org.hypertrace.core.graphql.trace.schema.TraceResultSet; + +public class TraceFetcher extends InjectableDataFetcher { + + public TraceFetcher() { + super(TraceFetcherImpl.class); + } + + static final class TraceFetcherImpl implements DataFetcher> { + private final TraceRequestBuilder requestBuilder; + private final TraceDao traceDao; + + @Inject + TraceFetcherImpl(TraceRequestBuilder requestBuilder, TraceDao traceDao) { + this.requestBuilder = requestBuilder; + this.traceDao = traceDao; + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + return this.requestBuilder + .build( + contextFromEnvironment(environment), + environment.getArguments(), + environment.getSelectionSet()) + .flatMap(this.traceDao::getTraces) + .toCompletionStage() + .toCompletableFuture(); + } + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/DefaultTraceRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/DefaultTraceRequestBuilder.java new file mode 100644 index 00000000..7bd799b8 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/DefaultTraceRequestBuilder.java @@ -0,0 +1,79 @@ +package org.hypertrace.core.graphql.trace.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.Map; +import javax.inject.Inject; +import lombok.Value; +import lombok.experimental.Accessors; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceType; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceTypeArgument; +import org.hypertrace.core.graphql.utils.schema.GraphQlSelectionFinder; +import org.hypertrace.core.graphql.utils.schema.SelectionQuery; + +class DefaultTraceRequestBuilder implements TraceRequestBuilder { + + private final ResultSetRequestBuilder resultSetRequestBuilder; + private final ArgumentDeserializer argumentDeserializer; + private final GraphQlSelectionFinder selectionFinder; + + @Inject + DefaultTraceRequestBuilder( + ResultSetRequestBuilder resultSetRequestBuilder, + ArgumentDeserializer argumentDeserializer, + GraphQlSelectionFinder selectionFinder) { + this.resultSetRequestBuilder = resultSetRequestBuilder; + this.argumentDeserializer = argumentDeserializer; + this.selectionFinder = selectionFinder; + } + + @Override + public Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet) { + + TraceType traceType = + this.argumentDeserializer + .deserializePrimitive(arguments, TraceTypeArgument.class) + .orElseThrow(); + + boolean fetchTotal = + this.selectionFinder + .findSelections( + selectionSet, SelectionQuery.namedChild(ResultSet.RESULT_SET_TOTAL_NAME)) + .count() + > 0; + + return this.build(context, traceType, arguments, selectionSet, fetchTotal); + } + + private Single build( + GraphQlRequestContext context, + TraceType traceType, + Map arguments, + DataFetchingFieldSelectionSet selectionSet, + boolean fetchTotal) { + + return this.resultSetRequestBuilder + .build(context, traceType.getScopeString(), arguments, selectionSet) + .map( + resultSetRequest -> + new DefaultTraceRequest(context, resultSetRequest, traceType, fetchTotal)); + } + + @Value + @Accessors(fluent = true) + private static class DefaultTraceRequest implements TraceRequest { + GraphQlRequestContext context; + ResultSetRequest resultSetRequest; + TraceType traceType; + boolean fetchTotal; + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequest.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequest.java new file mode 100644 index 00000000..c8704aeb --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequest.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.trace.request; + +import org.hypertrace.core.graphql.common.request.ContextualRequest; +import org.hypertrace.core.graphql.common.request.ResultSetRequest; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceType; + +public interface TraceRequest extends ContextualRequest { + ResultSetRequest resultSetRequest(); + + TraceType traceType(); + + boolean fetchTotal(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestBuilder.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestBuilder.java new file mode 100644 index 00000000..f1779d4d --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestBuilder.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.graphql.trace.request; + +import graphql.schema.DataFetchingFieldSelectionSet; +import io.reactivex.rxjava3.core.Single; +import java.util.Map; +import org.hypertrace.core.graphql.context.GraphQlRequestContext; + +public interface TraceRequestBuilder { + + Single build( + GraphQlRequestContext context, + Map arguments, + DataFetchingFieldSelectionSet selectionSet); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestModule.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestModule.java new file mode 100644 index 00000000..c959c87e --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/request/TraceRequestModule.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.graphql.trace.request; + +import com.google.inject.AbstractModule; +import org.hypertrace.core.graphql.common.request.ResultSetRequestBuilder; +import org.hypertrace.core.graphql.deserialization.ArgumentDeserializer; + +public class TraceRequestModule extends AbstractModule { + @Override + protected void configure() { + bind(TraceRequestBuilder.class).to(DefaultTraceRequestBuilder.class); + + requireBinding(ArgumentDeserializer.class); + requireBinding(ResultSetRequestBuilder.class); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/Trace.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/Trace.java new file mode 100644 index 00000000..d82eb5b4 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/Trace.java @@ -0,0 +1,10 @@ +package org.hypertrace.core.graphql.trace.schema; + +import graphql.annotations.annotationTypes.GraphQLName; +import org.hypertrace.core.graphql.common.schema.attributes.AttributeQueryable; +import org.hypertrace.core.graphql.common.schema.id.Identifiable; + +@GraphQLName(Trace.TYPE_NAME) +public interface Trace extends AttributeQueryable, Identifiable { + String TYPE_NAME = "Trace"; +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceResultSet.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceResultSet.java new file mode 100644 index 00000000..5acba03a --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceResultSet.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.graphql.trace.schema; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.results.ResultSet; + +@GraphQLName(TraceResultSet.TYPE_NAME) +public interface TraceResultSet extends ResultSet { + String TYPE_NAME = "TraceResultSet"; + + @Override + @GraphQLField + @GraphQLNonNull + @GraphQLName(RESULT_SET_RESULTS_NAME) + List results(); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceSchema.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceSchema.java new file mode 100644 index 00000000..f4fda8dd --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/TraceSchema.java @@ -0,0 +1,33 @@ +package org.hypertrace.core.graphql.trace.schema; + +import graphql.annotations.annotationTypes.GraphQLDataFetcher; +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLName; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import java.util.List; +import org.hypertrace.core.graphql.common.schema.arguments.TimeRangeArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.filter.FilterArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.order.OrderArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.LimitArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.page.OffsetArgument; +import org.hypertrace.core.graphql.common.schema.results.arguments.space.SpaceArgument; +import org.hypertrace.core.graphql.trace.fetcher.TraceFetcher; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceType; +import org.hypertrace.core.graphql.trace.schema.arguments.TraceTypeArgument; + +public interface TraceSchema { + String TRACE_QUERY_NAME = "traces"; + + @GraphQLField + @GraphQLNonNull + @GraphQLName(TRACE_QUERY_NAME) + @GraphQLDataFetcher(TraceFetcher.class) + TraceResultSet traces( + @Deprecated @GraphQLName(TraceTypeArgument.ARGUMENT_NAME) TraceType type, + @GraphQLName(TimeRangeArgument.ARGUMENT_NAME) @GraphQLNonNull TimeRangeArgument between, + @GraphQLName(FilterArgument.ARGUMENT_NAME) List filterBy, + @GraphQLName(OrderArgument.ARGUMENT_NAME) List orderBy, + @GraphQLName(LimitArgument.ARGUMENT_NAME) int limit, + @GraphQLName(OffsetArgument.ARGUMENT_NAME) int offset, + @GraphQLName(SpaceArgument.ARGUMENT_NAME) String space); +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceType.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceType.java new file mode 100644 index 00000000..4ac69090 --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceType.java @@ -0,0 +1,17 @@ +package org.hypertrace.core.graphql.trace.schema.arguments; + +import graphql.annotations.annotationTypes.GraphQLName; + +// TODO temporary for backwards compatibility +@GraphQLName(TraceType.TYPE_NAME) +public enum TraceType { + TRACE, + API_TRACE, + BACKEND_TRACE; + + static final String TYPE_NAME = "TraceType"; + + public String getScopeString() { + return name(); + } +} diff --git a/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceTypeArgument.java b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceTypeArgument.java new file mode 100644 index 00000000..921e5bbc --- /dev/null +++ b/hypertrace-core-graphql/hypertrace-core-graphql-trace-schema/src/main/java/org/hypertrace/core/graphql/trace/schema/arguments/TraceTypeArgument.java @@ -0,0 +1,8 @@ +package org.hypertrace.core.graphql.trace.schema.arguments; + +import org.hypertrace.core.graphql.deserialization.PrimitiveArgument; + +public interface TraceTypeArgument extends PrimitiveArgument { + // TODO figure out how to separate schema definition fo types + String ARGUMENT_NAME = "type"; +} diff --git a/hypertrace-core-graphql/owasp-suppressions.xml b/hypertrace-core-graphql/owasp-suppressions.xml new file mode 100644 index 00000000..204a52b4 --- /dev/null +++ b/hypertrace-core-graphql/owasp-suppressions.xml @@ -0,0 +1,11 @@ + + + + + ^pkg:maven/org\.hypertrace\..*@.*$ + cpe:/a:grpc:grpc + cpe:/a:utils_project:utils + + diff --git a/hypertrace-core-graphql/semantic-build-versioning.gradle b/hypertrace-core-graphql/semantic-build-versioning.gradle new file mode 100644 index 00000000..9bc16767 --- /dev/null +++ b/hypertrace-core-graphql/semantic-build-versioning.gradle @@ -0,0 +1,11 @@ +// Follows https://www.conventionalcommits.org/en/v1.0.0/#summary with one change: any commit is treated as a release, +// patch being the default if major or minor is not detected. + +autobump { + // match any message starting with a type/scope suffixed with !, or with a line starting with "BREAKING CHANGE:" + majorPattern = ~/(?m)(\A[^:]+(?<=!): |^BREAKING CHANGE:)/ + // match any commit message starting with "feat: " or "feat(any scope): " + minorPattern = ~/^feat(\([^)]+\))?: / + newPreReleasePattern = null // Not used - no prereleases + promoteToReleasePattern = null // Not used - every merge is a release +} \ No newline at end of file diff --git a/hypertrace-core-graphql/settings.gradle.kts b/hypertrace-core-graphql/settings.gradle.kts new file mode 100644 index 00000000..6975815e --- /dev/null +++ b/hypertrace-core-graphql/settings.gradle.kts @@ -0,0 +1,42 @@ +import org.hypertrace.gradle.dependency.DependencyPluginSettingExtension + +rootProject.name = "hypertrace-core-graphql-root" + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven("https://hypertrace.jfrog.io/artifactory/maven") + } +} + +plugins { + id("org.hypertrace.version-settings") version "0.2.1" + id("org.hypertrace.dependency-settings") version "0.1.2" +} + +configure { + catalogVersion.set("0.3.23") +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +include(":hypertrace-core-graphql-service") +include(":hypertrace-core-graphql-impl") +include(":hypertrace-core-graphql-spi") +include(":hypertrace-core-graphql-context") +include(":hypertrace-core-graphql-schema-registry") +include(":hypertrace-core-graphql-attribute-store") +include(":hypertrace-core-graphql-deserialization") +include(":hypertrace-core-graphql-grpc-utils") +include(":hypertrace-core-graphql-schema-utils") +include(":hypertrace-core-graphql-gateway-service-utils") +include(":hypertrace-core-graphql-common-schema") +include(":hypertrace-core-graphql-metadata-schema") +include(":hypertrace-core-graphql-span-schema") +include(":hypertrace-core-graphql-trace-schema") +include(":hypertrace-core-graphql-attribute-scope") +include(":hypertrace-core-graphql-attribute-scope-constants") +include(":hypertrace-core-graphql-rx-utils") +include(":hypertrace-core-graphql-log-event-schema") +include(":hypertrace-core-graphql-request-transformation")