Skip to main content

Local debugging guide — Go

Last updated on April 14, 2026

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

Based on the Extend App template

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


Prerequisites

See the Go 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 / PackageWhat it does
main.goEntry point — wires together the gRPC server, Prometheus metrics, OpenTelemetry tracing, and IAM login.
pkg/service/loginHandler.goYour business logic for userLoggedIn events — receives the event and calls grantEntitlement.
pkg/service/thirdPartyLoginHandler.goYour business logic for userThirdPartyLoggedIn events — same pattern as loginHandler.go.
pkg/service/entitlement.goShared helper that calls the AGS Fulfillment API to grant an item to a user.
pkg/common/scope.goRequest scope — bundles context, OpenTelemetry span, and a trace-tagged logger for each handler call.
pkg/common/logging.goBridges the gRPC middleware logger to Go's slog.
pkg/common/tracerProvider.goSets up OpenTelemetry distributed tracing (Zipkin exporter).
pkg/proto/.proto files describing AGS event message shapes.
pkg/pb/Auto-generated Go code from .proto. Do not edit directly.

Port numbers (constants in main.go):

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)

go run main.go

From VS Code

Use Terminal → Run Task → "Run: Service". This task is defined in .vscode/tasks.json and reads the .env file for you.

Confirming the service is up

You should see logs like:

{"time":"...","level":"INFO","msg":"starting app server.."}
{"time":"...","level":"INFO","msg":"gRPC reflection enabled"}
{"time":"...","level":"INFO","msg":"serving prometheus metrics","port":8080,"endpoint":"/metrics"}
{"time":"...","level":"INFO","msg":"gRPC server started"}
{"time":"...","level":"INFO","msg":"app server started"}

Confirm gRPC reflection (and therefore the server) 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

The repository ships with a ready-to-use launch configuration in .vscode/launch.json:

{
"name": "Debug: Service",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}

Steps:

Follow the attaching the debugger steps in the generic guide, then select "Debug: Service" from the dropdown.

The Go extension uses Delve under the hood. You do not need to configure it separately.

Other IDEs — Delve headless mode

Start Delve in headless mode so your IDE can connect to it:

export $(grep -v '^#' .env | xargs)
dlv debug --headless --listen=:2345 --api-version=2 .

Then configure your IDE to attach to localhost:2345 via DAP (Debug Adapter Protocol).


Where to put breakpoints

Most of the interesting logic happens in the service handler files. Start there.

What you want to investigateFile and location
Any login event arrivingpkg/service/loginHandler.go — top of OnMessage
Third-party login event arrivingpkg/service/thirdPartyLoginHandler.go — top of OnMessage
Entitlement grant logicpkg/service/entitlement.go — inside grantEntitlement
Service not starting at allmain.go — just before os.Exit(1) calls
Why a span or trace ID is missingpkg/common/scope.goChildScopeFromRemoteScope

Inspecting the incoming event payload

When the debugger pauses inside OnMessage, the msg parameter contains the full event. Expand it in the Variables panel to see all fields the event carries — UserId, Namespace, PlatformId, and others depending on the event type.

Conditional breakpoint syntax

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

msg.UserId == "test-user-001"

For more conditional breakpoint syntax examples, see the Go 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 Go language guide.

Tracing a single event end-to-end

Every OnMessage call creates a Scope (see pkg/common/scope.go) that attaches a traceID to every log line it emits. After sending a grpcurl request, grep for that trace ID to see all log lines from that one event:

go run main.go 2>&1 | jq -r 'select(.traceID != null) | "\(.traceID) \(.level) \(.msg)"'

Go-specific troubleshooting

Proto changes have no effect

Symptom: You edited a file in pkg/proto/ but nothing changed at runtime.

Cause: The generated stubs in pkg/pb/ have not been regenerated.

Fix: Run the "Proto: Generate" VS Code task, or:

./proto.sh

Then restart the service.


go run vs the debugger launch config

go run main.go compiles without debug information. When you need to attach a debugger, always use the VS Code launch config (or dlv debug) rather than go run.


Checking for port conflicts

If you see "address already in use":

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

Kill the stale process and start again.


grpcurl returns "Failed to dial"

Symptom:

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

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

Fix:

  1. Check the service logs for startup errors ("level":"ERROR" lines).
  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 you do not see a corresponding ERROR line in the logs.

Cause: Log level is set above error for some log lines, or the error is returned from a deep library call whose log output goes to stderr rather than the structured logger.

Fix:

  1. Set LOG_LEVEL=debug and rerun — this enables all log levels.
  2. Set a breakpoint in pkg/service/entitlement.go inside grantEntitlement to inspect the err value returned by FulfillItemShort.

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