diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 06fb8172..31015910 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -20,6 +20,15 @@ updates: patterns: - "*" + - package-ecosystem: "gradle" + directory: "/examples/opentelemetry" + schedule: + interval: "monthly" + groups: + dependencies: + patterns: + - "*" + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.gitignore b/.gitignore index c3784f2d..7da40f54 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ VERSION.txt # VSCode IDE /.vscode + +# env files +.env + +# mac +.DS_Store \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..47238df4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +## Examples of using the OpenFGA Java SDK + +A collection of examples demonstrating how to use the OpenFGA Java SDK in different scenarios. + +### Available Examples + +#### Basic Examples (`basic-examples/`) +A simple example that creates a store, runs a set of calls against it including creating a model, writing tuples and checking for access. This example is implemented in both Java and Kotlin. + +#### OpenTelemetry Examples +- `opentelemetry/` - Demonstrates OpenTelemetry integration both via manual code configuration, as well as no-code instrumentation using the OpenTelemetry java agent diff --git a/examples/opentelemetry/.env.example b/examples/opentelemetry/.env.example new file mode 100644 index 00000000..66de874e --- /dev/null +++ b/examples/opentelemetry/.env.example @@ -0,0 +1,19 @@ +# OpenFGA Configuration (REQUIRED) +FGA_API_URL=api_url_here +FGA_STORE_ID=store_id_here +FGA_MODEL_ID=model_id_here + +# Authentication (optional - for authenticated OpenFGA instances) +FGA_CLIENT_ID=client_id_here +FGA_CLIENT_SECRET=client_secret_here +FGA_API_AUDIENCE=api_audience_here +FGA_API_TOKEN_ISSUER=api_issuer_here + +# OpenTelemetry Configuration (for manual configuration mode - ./gradlew run) +# These are used when running with manual OpenTelemetry setup +# Note: When using the Java agent (./gradlew runWithAgent), +# these values are overridden by the JVM arguments in build.gradle +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +OTEL_SERVICE_NAME=openfga-java-sdk-example +OTEL_SERVICE_VERSION=1.0.0 + diff --git a/examples/opentelemetry/Makefile b/examples/opentelemetry/Makefile new file mode 100644 index 00000000..fbff300d --- /dev/null +++ b/examples/opentelemetry/Makefile @@ -0,0 +1,16 @@ +all: build + +openfga_version=latest + +build: + ./gradlew build + +run: + ./gradlew run + +run-with-agent: + ./gradlew runWithAgent + +run-openfga: + docker pull docker.io/openfga/openfga:${openfga_version} && \ + docker run -p 8080:8080 docker.io/openfga/openfga:${openfga_version} run diff --git a/examples/opentelemetry/README.md b/examples/opentelemetry/README.md new file mode 100644 index 00000000..02808abb --- /dev/null +++ b/examples/opentelemetry/README.md @@ -0,0 +1,206 @@ +# OpenTelemetry Example for OpenFGA Java SDK + +This example demonstrates two approaches for using OpenTelemetry metrics with the OpenFGA Java SDK: + +1. **Manual Configuration** (`./gradlew run`) - Code-based OpenTelemetry setup +2. **Java Agent** (`./gradlew runWithAgent`) - Zero-code automatic instrumentation + +Both approaches generate the same metrics: +- `fga-client.request.duration` - Total request time for FGA requests +- `fga-client.query.duration` - Time taken by FGA server to process requests +- `fga-client.credentials.request` - Number of token requests (if using client credentials) + +## SDK Version Configuration + +**By default**, this example uses a published version of the OpenFGA Java SDK. + +If you're contributing to the SDK or testing unreleased features: + +1. **Enable local SDK** in `settings.gradle`: + ```gradle + // Uncomment this line: + includeBuild '../..' + ``` + +2. **Update dependency** in `build.gradle`: + ```gradle + // Comment out the versioned dependency: + // implementation("dev.openfga:openfga-sdk:$fgaSdkVersion") + + // Uncomment the local dependency: + implementation("dev.openfga:openfga-sdk") + ``` + +3. **Build the main SDK first** (from repository root): + ```bash + cd ../.. + ./gradlew build + cd examples/opentelemetry + ``` + +## Prerequisites + +- Java 11 or higher +- Docker and Docker Compose +- OpenFGA server running (or use the provided docker-compose setup) + +## Quick Start + +### 1. Start the OpenTelemetry Stack + +```bash +# Clone the OpenTelemetry Collector setup +git clone https://github.com/ewanharris/opentelemetry-collector-dev-setup.git otel-collector +cd otel-collector + +# Start the services +docker-compose up -d +``` + +This provides: +- **Jaeger** at http://localhost:16686 - Distributed tracing UI +- **Prometheus** at http://localhost:9090 - Metrics collection and querying +- **Grafana** at http://localhost:3001 - Metrics visualization (admin:admin) + +### 2. Configure OpenFGA Connection + +Copy and edit the environment file: +```bash +cp .env.example .env +# Edit .env with your OpenFGA store details +``` + +### 3. Choose Your Approach + +#### Option A: Manual Configuration (./gradlew run) +```bash +./gradlew run +``` + +**Pros:** +- Full control over OpenTelemetry configuration +- Can customize metrics, exporters, and resources in code +- No external dependencies beyond your application + +**Cons:** +- Requires OpenTelemetry SDK dependencies in your application +- More code to write and maintain + +#### Option B: Java Agent (./gradlew runWithAgent) +```bash +./gradlew runWithAgent +``` + +**Pros:** +- Zero code changes required - completely automatic +- No OpenTelemetry dependencies needed in your application +- Easy to enable/disable by adding/removing the agent + +**Cons:** +- Less control over configuration +- Requires downloading and managing the agent JAR + +## Viewing Metrics + +Both approaches export metrics to the same OTLP endpoint. View them in: + +- **Prometheus**: http://localhost:9090/graph + - Query: `fga_client_request_duration_bucket` + - Query: `fga_client_query_duration_bucket` + - Query: `fga_client_credentials_request_total` + +- **Grafana**: http://localhost:3001 (admin:admin) + - Import dashboard from `grafana/` directory + - Or create custom dashboards with the FGA metrics + +## Architecture + +### Manual Configuration Mode +``` +Your App → OpenTelemetry SDK → OTLP Exporter → Collector → Prometheus/Jaeger +``` + +The application code: +1. Configures OpenTelemetry SDK with OTLP exporter +2. Creates OpenFGA client with default telemetry enabled +3. Performs FGA operations which generate metrics +4. Metrics are exported to the OTLP collector + +### Java Agent Mode +``` +Your App → OpenTelemetry Agent → OTLP Exporter → Collector → Prometheus/Jaeger +``` + +The OpenTelemetry agent: +1. Automatically detects and instruments the OpenFGA SDK +2. Configures exporters based on system properties +3. Collects metrics without any code changes +4. Exports to the same OTLP collector + +## Troubleshooting + +### No Metrics Appearing +1. Verify OTLP collector is running on localhost:4317 +2. Check the application logs for OpenTelemetry initialization messages +3. Ensure FGA operations are actually being performed + +### Manual Configuration Issues +- Verify all OpenTelemetry dependencies are included +- Check that `buildAndRegisterGlobal()` is called before creating the FGA client + +### Java Agent Issues +- Verify the agent JAR was downloaded successfully +- Check that OTEL system properties are set correctly +- Ensure the agent is being loaded (look for agent startup messages) + +### Connection Issues +- Verify your `.env` file has correct FGA_STORE_ID and FGA_MODEL_ID +- Check that your OpenFGA server is accessible +- Verify authentication credentials if using a protected OpenFGA instance + +## Observing Metrics + +### Prometheus (http://localhost:9090) + +Query for OpenFGA metrics: +- `fga_client_request_duration_bucket` - Request duration histogram +- `fga_client_query_duration_bucket` - Query duration histogram +- `fga_client_credentials_request_total` - Credentials request counter + +Example queries: +```promql +# Average request duration by method +rate(fga_client_request_duration_sum[5m]) / rate(fga_client_request_duration_count[5m]) + +# Request rate by HTTP status code +rate(fga_client_request_duration_count[5m]) + +# 95th percentile request duration +histogram_quantile(0.95, rate(fga_client_request_duration_bucket[5m])) +``` + +### Grafana (http://localhost:3001) + +Login with `admin:admin`. The collector setup includes pre-configured dashboards for OpenFGA metrics. + +## Next Steps + +- Explore the metrics in Grafana with custom dashboards +- Try different telemetry configurations to see what works best for your use case +- Consider which approach (manual vs agent) fits better with your deployment strategy + +## Cleanup + +To stop the OpenTelemetry stack: + +```bash +cd otel-collector +docker-compose down +``` + +## Learn More + +- [OpenFGA Documentation](https://openfga.dev/docs) +- [OpenFGA Java SDK Documentation](../../README.md) +- [OpenTelemetry Java Documentation](https://opentelemetry.io/docs/languages/java/) +- [OpenFGA Telemetry Documentation](../../docs/OpenTelemetry.md) diff --git a/examples/opentelemetry/build.gradle b/examples/opentelemetry/build.gradle new file mode 100644 index 00000000..c8c76e1c --- /dev/null +++ b/examples/opentelemetry/build.gradle @@ -0,0 +1,115 @@ +plugins { + id 'application' + id 'com.diffplug.spotless' version '7.2.1' +} + +application { + mainClass = 'dev.openfga.sdk.example.opentelemetry.OpenTelemetryExample' +} + +// Override the default run task to pass manual config flag +run { + args = ['--mode=manual'] +} + +// Task to download OpenTelemetry Java agent if not present +task downloadAgent { + group = 'setup' + description = 'Download OpenTelemetry Java agent if not present' + + doLast { + def agentFile = file('opentelemetry-javaagent.jar') + if (!agentFile.exists()) { + println "Downloading OpenTelemetry Java agent..." + def agentUrl = 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar' + try { + new URL(agentUrl).withInputStream { i -> + agentFile.withOutputStream { it << i } + } + println "Downloaded opentelemetry-javaagent.jar" + } catch (Exception e) { + throw new GradleException("Failed to download OpenTelemetry agent: ${e.message}", e) + } + println "Downloaded opentelemetry-javaagent.jar" + } else { + println "OpenTelemetry agent already exists" + } + } +} + +// Task to run with OpenTelemetry agent (no-code approach) +task runWithAgent(type: JavaExec) { + group = 'application' + description = 'Run the OpenTelemetry example with Java agent (no-code approach)' + dependsOn downloadAgent + classpath = sourceSets.main.runtimeClasspath + mainClass = 'dev.openfga.sdk.example.opentelemetry.OpenTelemetryExample' + + // Add JVM arguments for OpenTelemetry agent with configuration + jvmArgs = [ + '-javaagent:opentelemetry-javaagent.jar', + '-Dotel.service.name=openfga-java-sdk-agent-example', + '-Dotel.service.version=1.0.0', + '-Dotel.exporter.otlp.endpoint=http://localhost:4317', + '-Dotel.exporter.otlp.protocol=grpc' + ] + + // Pass agent mode flag to the application + args = ['--mode=agent'] + + doFirst { + println "🤖 Running with OpenTelemetry Java agent (no-code approach)..." + println "Service Name: openfga-java-sdk-agent-example" + println "Service Version: 1.0.0" + println "Exporter Endpoint: http://localhost:4317" + println "Exporter Protocol: grpc" + println "" + println "Make sure you have an OTLP collector running on localhost:4317" + println "The agent automatically instruments the application - no code changes needed!" + } +} + +repositories { + mavenCentral() +} + +ext { + fgaSdkVersion = "0.9.0" + openTelemetryVersion = "1.53.0" + openTelemetryAlphaVersion = "1.53.0-alpha" +} + +dependencies { + // Core FGA SDK (always required) + // By default, uses the published SDK from Maven Central + implementation("dev.openfga:openfga-sdk:$fgaSdkVersion") + + // For local development using the SDK source code: + // 1. Uncomment the includeBuild line in settings.gradle + // 2. Comment out the line above and uncomment the line below: + // implementation("dev.openfga:openfga-sdk") + + // OpenTelemetry SDK dependencies - ONLY NEEDED FOR MANUAL CONFIGURATION (./gradlew run) + // When using the Java agent (./gradlew runWithAgent), these dependencies are not required + // The agent provides all OpenTelemetry functionality automatically + implementation("io.opentelemetry:opentelemetry-sdk:$openTelemetryVersion") + implementation("io.opentelemetry:opentelemetry-exporter-prometheus:$openTelemetryAlphaVersion") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:$openTelemetryVersion") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:$openTelemetryVersion") + implementation("io.opentelemetry.semconv:opentelemetry-semconv:1.34.0") + + // Environment variables for this example + implementation("io.github.cdimascio:dotenv-java:3.2.0") +} + +// Use spotless plugin to automatically format code +spotless { + enforceCheck false + java { + palantirJavaFormat() + removeUnusedImports() + importOrder() + } +} + + diff --git a/examples/opentelemetry/gradle/wrapper/gradle-wrapper.jar b/examples/opentelemetry/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/examples/opentelemetry/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/opentelemetry/gradle/wrapper/gradle-wrapper.properties b/examples/opentelemetry/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a80b22ce --- /dev/null +++ b/examples/opentelemetry/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/opentelemetry/gradlew b/examples/opentelemetry/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/examples/opentelemetry/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/examples/opentelemetry/gradlew.bat b/examples/opentelemetry/gradlew.bat new file mode 100644 index 00000000..7101f8e4 --- /dev/null +++ b/examples/opentelemetry/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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/examples/opentelemetry/settings.gradle b/examples/opentelemetry/settings.gradle new file mode 100644 index 00000000..b73b6150 --- /dev/null +++ b/examples/opentelemetry/settings.gradle @@ -0,0 +1,10 @@ +rootProject.name = 'opentelemetry-example' + +// By default, this example uses the published SDK from Maven Central. +// To use the local SDK source code instead: +// 1. Uncomment the includeBuild line below +// 2. Remove the version number from the SDK dependency in build.gradle + +// includeBuild '../..' + + diff --git a/examples/opentelemetry/src/main/java/dev/openfga/sdk/example/opentelemetry/OpenTelemetryExample.java b/examples/opentelemetry/src/main/java/dev/openfga/sdk/example/opentelemetry/OpenTelemetryExample.java new file mode 100644 index 00000000..b85ec980 --- /dev/null +++ b/examples/opentelemetry/src/main/java/dev/openfga/sdk/example/opentelemetry/OpenTelemetryExample.java @@ -0,0 +1,393 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.example.opentelemetry; + +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.model.*; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import dev.openfga.sdk.api.configuration.ClientCredentials; +import dev.openfga.sdk.api.configuration.Credentials; +import dev.openfga.sdk.api.configuration.TelemetryConfiguration; +import dev.openfga.sdk.api.model.*; +import dev.openfga.sdk.telemetry.Attribute; +import dev.openfga.sdk.telemetry.Attributes; +import dev.openfga.sdk.telemetry.Counters; +import dev.openfga.sdk.telemetry.Histograms; +import dev.openfga.sdk.telemetry.Metric; +import io.github.cdimascio.dotenv.Dotenv; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * OpenFGA Java SDK - Combined OpenTelemetry Example + * + * This example demonstrates two approaches for using OpenTelemetry metrics with the OpenFGA Java SDK: + * + * 1. MANUAL CONFIGURATION (./gradlew run): + * - Uses code-based OpenTelemetry setup + * - Requires OpenTelemetry SDK dependencies + * - Full control over configuration + * + * 2. JAVA AGENT (./gradlew runWithAgent): + * - Uses OpenTelemetry Java agent for automatic instrumentation + * - No OpenTelemetry dependencies required in your application + * - Zero-code configuration approach + * + * Both approaches generate the same metrics: + * - fga-client.request.duration (histogram) - Total request time including network latency + * - fga-client.query.duration (histogram) - FGA server processing time only + * - fga-client.credentials.request (counter) - Number of authentication token requests + */ +public class OpenTelemetryExample { + + // Application Configuration + private static final int OPERATION_LOOP_INTERVAL_SECONDS = 20; + private static final int ERROR_RETRY_INTERVAL_SECONDS = 2; + private static final int METRICS_EXPORT_INTERVAL_SECONDS = 10; + + // Command Line Arguments + private static final String ARG_MODE_AGENT = "--mode=agent"; + private static final String ARG_MODE_MANUAL = "--mode=manual"; + + // Environment Variable Names + private static final String ENV_OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"; + private static final String ENV_OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME"; + private static final String ENV_OTEL_SERVICE_VERSION = "OTEL_SERVICE_VERSION"; + private static final String ENV_FGA_API_URL = "FGA_API_URL"; + private static final String ENV_FGA_STORE_ID = "FGA_STORE_ID"; + private static final String ENV_FGA_MODEL_ID = "FGA_MODEL_ID"; + private static final String ENV_FGA_CLIENT_ID = "FGA_CLIENT_ID"; + private static final String ENV_FGA_CLIENT_SECRET = "FGA_CLIENT_SECRET"; + private static final String ENV_FGA_API_AUDIENCE = "FGA_API_AUDIENCE"; + private static final String ENV_FGA_API_TOKEN_ISSUER = "FGA_API_TOKEN_ISSUER"; + + // Default Values + private static final String DEFAULT_OTLP_ENDPOINT = "http://localhost:4317"; + private static final String DEFAULT_SERVICE_NAME = "openfga-java-sdk-example"; + private static final String DEFAULT_SERVICE_VERSION = "1.0.0"; + private static final String DEFAULT_FGA_API_URL = "http://localhost:8080"; + private static final String DEFAULT_API_AUDIENCE = "https://api.fga.example"; + private static final String DEFAULT_API_TOKEN_ISSUER = "auth.fga.example"; + + private static Dotenv dotenv; + private static OpenFgaClient fgaClient; + + public static void main(String[] args) throws Exception { + System.out.println("🚀 OpenFGA Java SDK - OpenTelemetry Example"); + System.out.println("==========================================="); + + // Determine OpenTelemetry mode from command line arguments + boolean isAgentMode = determineOpenTelemetryMode(args); + + // Load environment variables + dotenv = Dotenv.configure().ignoreIfMissing().load(); + + if (isAgentMode) { + System.out.println("🤖 Java Agent Mode"); + System.out.println(" The OpenTelemetry Java agent handles all setup automatically"); + System.out.println(" No configuration code needed in your application"); + } else { + System.out.println("🔧 Manual Configuration Mode"); + System.out.println(" This example shows code-based OpenTelemetry setup"); + System.out.println(" Requires OpenTelemetry SDK dependencies"); + + // Configure OpenTelemetry manually - ONLY NEEDED FOR MANUAL MODE + configureOpenTelemetryManually(); + } + + // Create OpenFGA client (works the same for both modes) + createOpenFgaClient(); + + System.out.println("\n🔄 Starting continuous operations loop..."); + System.out.println( + " Operations will run every " + OPERATION_LOOP_INTERVAL_SECONDS + " seconds until stopped (Ctrl+C)"); + System.out.println(" This matches the behavior of other OpenFGA SDK examples"); + + // Run operations continuously + int operationCount = 0; + while (true) { + try { + operationCount++; + System.out.println("\n--- Operation " + operationCount + " ---"); + performOperations(); + + // Wait before next iteration + TimeUnit.SECONDS.sleep(OPERATION_LOOP_INTERVAL_SECONDS); + } catch (Exception e) { + System.err.println("Error in operation " + operationCount + ": " + e.getMessage()); + e.printStackTrace(); + TimeUnit.SECONDS.sleep(ERROR_RETRY_INTERVAL_SECONDS); + } + } + } + + /** + * Determine the OpenTelemetry mode from command line arguments + * @param args Command line arguments + * @return true if agent mode, false if manual configuration mode + */ + private static boolean determineOpenTelemetryMode(String[] args) { + System.out.println("🔧 Parsing arguments: " + java.util.Arrays.toString(args)); + + for (String arg : args) { + if (ARG_MODE_AGENT.equals(arg)) { + System.out.println("✓ Agent mode detected from arguments"); + return true; + } else if (ARG_MODE_MANUAL.equals(arg)) { + System.out.println("✓ Manual mode detected from arguments"); + return false; + } + } + + // Default to manual mode if no argument provided + if (args.length == 0) { + System.out.println("ℹ️ No mode specified, defaulting to manual configuration"); + } + return false; + } + + /** + * MANUAL CONFIGURATION ONLY - configures OpenTelemetry programmatically + * This method is only called when NOT using the Java agent + */ + private static void configureOpenTelemetryManually() { + System.out.println("\n⚙️ Configuring OpenTelemetry manually..."); + + String otlpEndpoint = dotenv.get(ENV_OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_OTLP_ENDPOINT); + String serviceName = dotenv.get(ENV_OTEL_SERVICE_NAME, DEFAULT_SERVICE_NAME); + String serviceVersion = dotenv.get(ENV_OTEL_SERVICE_VERSION, DEFAULT_SERVICE_VERSION); + + System.out.println(" OTLP Endpoint: " + otlpEndpoint); + System.out.println(" Service Name: " + serviceName); + System.out.println(" Service Version: " + serviceVersion); + + // Create resource with service information + Resource resource = Resource.getDefault().toBuilder() + .put(ServiceAttributes.SERVICE_NAME, serviceName) + .put(ServiceAttributes.SERVICE_VERSION, serviceVersion) + .build(); + + // Configure OTLP metric exporter + OtlpGrpcMetricExporter metricExporter = + OtlpGrpcMetricExporter.builder().setEndpoint(otlpEndpoint).build(); + + // Create meter provider with OTLP exporter + SdkMeterProvider meterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(metricExporter) + .setInterval( + Duration.ofSeconds(METRICS_EXPORT_INTERVAL_SECONDS)) // Export metrics every 10 seconds + .build()) + .setResource(resource) + .build(); + + // Build and register OpenTelemetry SDK globally + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(meterProvider).buildAndRegisterGlobal(); + + System.out.println(" ✅ OpenTelemetry SDK configured with OTLP exporter"); + System.out.println(" 📊 Metrics will be exported to OTLP endpoint: " + otlpEndpoint); + } + + /** + * Create a comprehensive telemetry configuration showing all available attributes + * This demonstrates how to customize which telemetry data the SDK collects + */ + private static TelemetryConfiguration createTelemetryConfiguration() { + System.out.println("\n📊 Configuring comprehensive telemetry attributes..."); + + // Create a comprehensive attribute map that includes ALL available attributes + // This goes beyond the default configuration to show every telemetry option + Map> allAttributes = new HashMap<>(); + + // ✅ DEFAULT ATTRIBUTES - These are enabled by default in TelemetryConfiguration() + allAttributes.put(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, Optional.empty()); + allAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, Optional.empty()); + allAttributes.put(Attributes.FGA_CLIENT_REQUEST_MODEL_ID, Optional.empty()); + allAttributes.put(Attributes.FGA_CLIENT_REQUEST_STORE_ID, Optional.empty()); + allAttributes.put(Attributes.FGA_CLIENT_RESPONSE_MODEL_ID, Optional.empty()); + allAttributes.put(Attributes.HTTP_HOST, Optional.empty()); + allAttributes.put(Attributes.HTTP_REQUEST_METHOD, Optional.empty()); + allAttributes.put(Attributes.HTTP_REQUEST_RESEND_COUNT, Optional.empty()); + allAttributes.put(Attributes.HTTP_RESPONSE_STATUS_CODE, Optional.empty()); + allAttributes.put(Attributes.URL_FULL, Optional.empty()); + allAttributes.put(Attributes.URL_SCHEME, Optional.empty()); + allAttributes.put(Attributes.USER_AGENT, Optional.empty()); + + // 🔧 ADDITIONAL ATTRIBUTES - These are NOT enabled by default + allAttributes.put(Attributes.FGA_CLIENT_REQUEST_BATCH_CHECK_SIZE, Optional.empty()); + allAttributes.put(Attributes.FGA_CLIENT_USER, Optional.empty()); + + // Create metrics configuration with all attributes + Map>> comprehensiveMetrics = new HashMap<>(); + comprehensiveMetrics.put(Histograms.REQUEST_DURATION, allAttributes); + comprehensiveMetrics.put(Histograms.QUERY_DURATION, allAttributes); + comprehensiveMetrics.put(Counters.CREDENTIALS_REQUEST, allAttributes); + + TelemetryConfiguration telemetryConfig = new TelemetryConfiguration(comprehensiveMetrics); + + System.out.println(" 📊 All SDK metrics enabled: REQUEST_DURATION, QUERY_DURATION, CREDENTIALS_REQUEST"); + + return telemetryConfig; + } + + private static void createOpenFgaClient() throws Exception { + System.out.println("\n🔧 Creating OpenFGA client..."); + + String apiUrl = dotenv.get(ENV_FGA_API_URL, DEFAULT_FGA_API_URL); + String storeId = dotenv.get(ENV_FGA_STORE_ID); + String modelId = dotenv.get(ENV_FGA_MODEL_ID); + + if (storeId == null || modelId == null) { + throw new IllegalStateException( + ENV_FGA_STORE_ID + " and " + ENV_FGA_MODEL_ID + " must be configured in .env file"); + } + + System.out.println(" API URL: " + apiUrl); + System.out.println(" Store ID: " + storeId); + System.out.println(" Model ID: " + modelId); + + // Create client configuration with default telemetry + // The SDK will automatically detect and use either: + // - The globally registered OpenTelemetry instance (manual config) + // - The OpenTelemetry agent instance (agent mode) + ClientConfiguration config = new ClientConfiguration() + .apiUrl(apiUrl) + .storeId(storeId) + .authorizationModelId(modelId) + .telemetryConfiguration(createTelemetryConfiguration()); + + // Configure authentication if credentials are provided + String clientId = dotenv.get(ENV_FGA_CLIENT_ID); + String clientSecret = dotenv.get(ENV_FGA_CLIENT_SECRET); + + if (clientId != null && clientSecret != null) { + String apiAudience = dotenv.get(ENV_FGA_API_AUDIENCE, DEFAULT_API_AUDIENCE); + String apiTokenIssuer = dotenv.get(ENV_FGA_API_TOKEN_ISSUER, DEFAULT_API_TOKEN_ISSUER); + + config.credentials(new Credentials(new ClientCredentials() + .clientId(clientId) + .clientSecret(clientSecret) + .apiAudience(apiAudience) + .apiTokenIssuer(apiTokenIssuer))); + + System.out.println(" ✅ Client credentials authentication configured"); + } else { + System.out.println(" ℹ️ No authentication configured (using public API)"); + } + + // Create the client + fgaClient = new OpenFgaClient(config); + + System.out.println("✅ OpenFGA client created with telemetry enabled!"); + System.out.println(" 📊 Metrics will be automatically collected and exported"); + } + + private static void performOperations() throws Exception { + // Read the authorization model + System.out.println("📖 Reading authorization model..."); + ClientReadAuthorizationModelResponse modelResponse = + fgaClient.readAuthorizationModel().get(); + System.out.println("✓ Read authorization model: " + + modelResponse.getAuthorizationModel().getId()); + + // Write some test tuples first (to ensure we have data to check) + System.out.println("✍️ Writing test tuples..."); + try { + List tuples = List.of( + new ClientTupleKey().user("user:anne").relation("viewer")._object("doc:2021-budget"), + new ClientTupleKey().user("user:beth").relation("can_write")._object("doc:2021-budget"), + new ClientTupleKey().user("user:anne").relation("viewer")._object("doc:2022-budget")); + + ClientWriteResponse writeResponse = + fgaClient.write(new ClientWriteRequest().writes(tuples)).get(); + System.out.println("✓ Wrote " + tuples.size() + " test tuples"); + } catch (Exception writeError) { + System.out.println("⚠️ Could not write tuples (may not be needed): " + writeError.getMessage()); + } + + // Read existing tuples + System.out.println("📋 Reading existing tuples..."); + ClientReadResponse readResponse = + fgaClient.read(new ClientReadRequest()).get(); + System.out.println("✓ Found " + readResponse.getTuples().size() + " existing tuples"); + + // Perform check requests + System.out.println("🔍 Performing check operations..."); + + // Check: user:anne can view doc:2021-budget + ClientCheckRequest check1 = + new ClientCheckRequest().user("user:anne").relation("viewer")._object("doc:2021-budget"); + ClientCheckResponse checkResponse1 = fgaClient.check(check1).get(); + System.out.println("✓ Check user:anne can view doc:2021-budget: " + checkResponse1.getAllowed()); + + // Check: user:beth can write doc:2021-budget + ClientCheckRequest check2 = + new ClientCheckRequest().user("user:beth").relation("can_write")._object("doc:2021-budget"); + ClientCheckResponse checkResponse2 = fgaClient.check(check2).get(); + System.out.println("✓ Check user:beth can write doc:2021-budget: " + checkResponse2.getAllowed()); + + // Check: user:anne can view doc:2022-budget + ClientCheckRequest check3 = + new ClientCheckRequest().user("user:anne").relation("viewer")._object("doc:2022-budget"); + ClientCheckResponse checkResponse3 = fgaClient.check(check3).get(); + System.out.println("✓ Check user:anne can view doc:2022-budget: " + checkResponse3.getAllowed()); + + // Batch check operations + System.out.println("🔍 Performing batch check operations..."); + List batchChecks = List.of( + new ClientCheckRequest().user("user:anne").relation("viewer")._object("doc:2021-budget"), + new ClientCheckRequest().user("user:beth").relation("can_write")._object("doc:2021-budget"), + new ClientCheckRequest().user("user:anne").relation("viewer")._object("doc:2022-budget")); + + List batchResponse = + fgaClient.clientBatchCheck(batchChecks).get(); + System.out.println("✓ Batch check completed with " + batchResponse.size() + " results:"); + + for (int i = 0; i < batchResponse.size(); i++) { + ClientBatchCheckClientResponse response = batchResponse.get(i); + String correlationId = "check-" + (i + 1); + if (response.getThrowable() == null) { + System.out.println(" - " + correlationId + ": " + response.getAllowed()); + } else { + System.out.println(" - " + correlationId + ": ERROR - " + + response.getThrowable().getMessage()); + } + } + + // List objects operation + System.out.println("📋 Listing objects user:anne can view..."); + ClientListObjectsRequest listRequest = new ClientListObjectsRequest() + .user("user:anne") + .relation("viewer") + .type("doc"); + + ClientListObjectsResponse listResponse = + fgaClient.listObjects(listRequest).get(); + System.out.println("✓ user:anne can view " + listResponse.getObjects().size() + " documents: " + + String.join(", ", listResponse.getObjects())); + + System.out.println("📊 All operations completed - metrics generated!"); + System.out.println("📊 Generated metrics: request.duration, query.duration, credentials.request"); + } +}