Skip to main content

Local debugging guide — Java

Last updated on April 14, 2026

This guide covers everything specific to debugging an Extend Event Handler app written in Java. For general debugging concepts — environment setup, VS Code debug workflow, log reading, and common issues — see:

Based on the Extend App template

File paths, class names, and configuration files in this guide refer to the AccelByte Extend Event Handler Java template. If your project uses different names, the concepts still apply — adjust paths to match your layout.


Prerequisites

See the Java language setup guide for installation requirements. In addition, grpcurl is required for simulating incoming events during local development — see Triggering events for testing in the main guide for installation and usage.


Project structure

File / ClassWhat it does
src/main/java/net/accelbyte/Application.javaEntry point — bootstraps the Spring Boot application.
src/main/java/net/accelbyte/config/AppConfig.javaSpring configuration — validates ITEM_ID_TO_GRANT and creates the AccelByteSDK bean (calls loginClient() on startup).
src/main/java/net/accelbyte/service/LoginHandler.javaYour business logic for userLoggedIn events — extends the generated gRPC stub and calls grantEntitlement.
src/main/java/net/accelbyte/service/ThirdPartyLoginHandler.javaYour business logic for userThirdPartyLoggedIn events — same pattern as LoginHandler.
src/main/java/net/accelbyte/service/EntitlementService.javaShared Spring service that calls the AGS Fulfillment API to grant an item to a user.
src/main/java/net/accelbyte/grpc/DebugLoggerServerInterceptor.javaOptional gRPC interceptor that logs every request/response when plugin.grpc.server.interceptor.debug-logger.enabled=true.
src/main/java/net/accelbyte/grpc/ExceptionServerInterceptor.javagRPC interceptor that converts unhandled exceptions to proper gRPC status codes.
src/main/resources/application.ymlMain Spring Boot configuration — maps environment variables to application properties.
src/main/resources/application-local.ymlLocal-only profile overrides — enables the debug logger interceptor and uses logback-spring-local.xml.
src/main/proto/.proto files describing AGS event message shapes.
build.gradleBuild file — defines dependencies, the bootRun task (with OTEL agent), and the bootJar task.

Port numbers (configured in application.yml and Spring Boot defaults):

PortPurpose
6565gRPC server — receives events from Kafka Connect and simulated grpcurl calls
8080Prometheus metrics endpoint (/metrics)

Running the service locally

From the terminal

# Export all variables from your .env file
export $(grep -v '^#' .env | xargs)

./gradlew bootRun

The bootRun task (defined in build.gradle) automatically attaches the AWS OpenTelemetry agent and activates the local Spring profile, which enables the debug logger interceptor.

Confirming the service is up

You should see log output similar to this (formatted by Logstash JSON encoder):

{"@timestamp":"...","level":"INFO","message":"Starting Application using Java 17..."}
{"@timestamp":"...","level":"INFO","message":"LoginHandler initialized"}
{"@timestamp":"...","level":"INFO","message":"ThirdPartyLoginHandler initialized"}
{"@timestamp":"...","level":"INFO","message":"DebugLoggerServerInterceptor initialized"}
{"@timestamp":"...","level":"INFO","message":"Started Application in 3.x seconds"}

Confirm gRPC reflection is working:

grpcurl -plaintext localhost:6565 list

Expected output — a list of registered services:

accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService
accelbyte.iam.account.v1.UserAuthenticationUserThirdPartyLoggedInService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection

Attaching the debugger

Because the repository does not yet ship a .vscode/launch.json, create one now:

mkdir -p .vscode

Then create .vscode/launch.json with the following content:

{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Debug: Service",
"request": "attach",
"hostName": "localhost",
"port": 5005
}
]
}

Steps:

Follow the attaching the debugger steps in the generic guide. For Java, the debugger attaches over JDWP rather than launching directly. Start the service with the JDWP debug agent first:

export $(grep -v '^#' .env | xargs)
./gradlew bootRun --jvmArgs="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"

Wait until you see Started Application in in the output, open the Run and Debug panel, select "Debug: Service", and press F5 to attach.

The Java debugger connects over JDWP. Once attached, breakpoints set in any .java file will pause execution.

Other IDEs — JDWP attach mode

Start the service with the JDWP agent as shown above, then configure your IDE to attach to localhost:5005 using a Remote JVM Debug configuration (IntelliJ IDEA) or equivalent.

Suspend on startup (breakpoints in @PostConstruct or static initializers)

If you need to pause before the application initializes (e.g., to debug AppConfig), use suspend=y instead of suspend=n. The JVM will freeze until the debugger connects:

./gradlew bootRun --jvmArgs="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"

Where to put breakpoints

Most of the interesting logic is in the service classes and the config. Start there.

What you want to investigateFile and location
Any login event arrivingLoginHandler.java — top of onMessage
Third-party login event arrivingThirdPartyLoginHandler.java — top of onMessage
Entitlement grant logicEntitlementService.java — inside grantEntitlement
SDK login / credential validationAppConfig.java — inside provideAccelbyteSdk, after loginClient()
ITEM_ID_TO_GRANT validationAppConfig.java — inside validateItemIdToGrant
Inspect every raw gRPC requestDebugLoggerServerInterceptor.java — inside onMessage of the ForwardingServerCallListener

Inspecting the incoming event payload

When the debugger pauses inside onMessage, the request parameter contains the full event. Expand it in the Variables panel to see all fields — userId, namespace, platformId, and others depending on the event type.

Conditional breakpoint syntax

Right-click a breakpoint → Edit Breakpoint → enter a Java expression, for example:

request.getUserId().equals("test-user-001")

For more conditional breakpoint syntax examples, see the Java language guide.


Triggering events for testing

For event simulation options and grpcurl commands, see Triggering events for testing in the main guide.

Describe a service method

When you need to know the exact field names for a proto method, use grpcurl describe:

grpcurl -plaintext localhost:6565 \
describe accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService.OnMessage

This prints the full proto definition of the method — useful when you need to know exactly what fields to include in the -d payload.


Reading logs

For jq log filtering, see the Java language guide.

Trace IDs in logs

The application.yml configures the log pattern to include OpenTelemetry trace context:

trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %5p

After sending a grpcurl request, grep for the trace ID to see all log lines from that one event:

./gradlew bootRun 2>&1 | jq 'select(.trace_id != null and .trace_id != "") | {trace_id, level, message}'

Enabling the debug logger interceptor

The DebugLoggerServerInterceptor logs every raw gRPC request and response but is disabled by default. To enable it, set the property in application-local.yml (already there) or pass it on the command line:

./gradlew bootRun -Dplugin.grpc.server.interceptor.debug-logger.enabled=true

When enabled, each call logs:

{"level":"INFO","message":"Request path: accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService/OnMessage"}
{"level":"INFO","message":"Request headers: ..."}
{"level":"INFO","message":"Request message: userId: \"test-user-001\" namespace: \"mygame\""}

Java-specific troubleshooting

Service fails to start: ITEM_ID_TO_GRANT is not configured

Symptom:

Caused by: java.lang.IllegalArgumentException: Required envar ITEM_ID_TO_GRANT is not configured

Cause: The ITEM_ID_TO_GRANT environment variable is empty or not set.

Fix: Set ITEM_ID_TO_GRANT in your .env file to a valid item ID from a published store in your namespace, then re-export and restart.


Service fails to start: failed to sdk.loginClient()

Symptom:

Caused by: org.springframework.web.server.ServerErrorException: failed to sdk.loginClient()

Cause: AppConfig.provideAccelbyteSdk() could not authenticate with AGS. This happens when AB_BASE_URL, AB_CLIENT_ID, or AB_CLIENT_SECRET is wrong or missing.

Fix:

  1. Verify AB_BASE_URL is reachable: curl "$AB_BASE_URL/iam/v3/public/config".
  2. Confirm AB_CLIENT_ID and AB_CLIENT_SECRET are set and match an existing OAuth client.
  3. Set a breakpoint in AppConfig.provideAccelbyteSdk() to inspect the SDK state before loginClient() is called.

Debugger never pauses at a breakpoint

Symptom: The attach succeeds but breakpoints are never hit.

Causes and fixes:

  • Stale .class files — the class the debugger loaded does not match the source. Run ./gradlew clean bootRun --jvmArgs="..." to force a full recompile.
  • Wrong port — confirm the service is listening on 5005: ss -tlnp | grep 5005.
  • Hot-swap mismatch — the Java debugger supports limited hot-swap. If you edited source after attaching, restart the service and reattach.

Checking for port conflicts

If you see Address already in use:

ss -tlnp | grep -E '6565|8080|5005'

Kill the stale process and start again.


grpcurl returns "Failed to dial"

Symptom:

Failed to dial target host "localhost:6565": ...

Cause: The gRPC server has not started yet, crashed on startup, or is listening on a different port.

Fix:

  1. Check the service logs for ERROR lines — look for Spring startup failures.
  2. Confirm port 6565 is open: ss -tlnp | grep 6565.
  3. Rerun grpcurl -plaintext localhost:6565 list — if it succeeds, the server is up.

Handler returns INTERNAL but no ERROR log appears

Symptom: grpcurl reports Code: Internal but no ERROR line appears in the logs.

Cause: The exception is caught by ExceptionServerInterceptor before it reaches the structured logger, or the log level filters it out.

Fix:

  1. Set a breakpoint in EntitlementService.grantEntitlement on the catch (Exception e) block to inspect the exception before it propagates.
  2. Temporarily lower the log level: add logging.level.net.accelbyte=DEBUG to application-local.yml and restart.

Proto changes have no effect

Symptom: You edited a file in src/main/proto/ but the change has no effect at runtime.

Cause: The generated Java stubs have not been regenerated.

Fix: Run a clean build to trigger proto compilation:

./gradlew clean build

Then restart the service.


AI assistance

The app template ships with a Claude agent skill at .claude/skills/debugging-guide/SKILL.md. Copy the full skill file into your own repository and activate it in your AI assistant.

For the full skill content and prompting tips, see the AI assistance section in the main guide.


References