Debugging Techniques
Debugging Overview
The Delve debugger provides powerful capabilities for investigating provider behavior, inspecting variables, and stepping through code execution.
Delve Setup
Installation
# Install Delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Verify installation
dlv version
Basic Usage
# Debug provider binary
dlv debug ./main.go
# Debug with arguments
dlv debug ./main.go -- --debug
# Attach to running process
dlv attach <pid>
# Debug tests
dlv test ./internal/provider/
VSCode Integration
Launch Configuration
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Provider",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"args": ["--debug"],
"env": {
"TF_LOG": "DEBUG",
"TF_LOG_PROVIDER": "TRACE"
}
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/internal/provider",
"args": [
"-test.run",
"TestCacheResource_Create"
],
"env": {
"TF_ACC": "1"
}
},
{
"name": "Attach to Provider",
"type": "go",
"request": "attach",
"mode": "local",
"processId": "${command:pickProcess}"
}
]
}
Tasks Configuration
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build-provider",
"type": "shell",
"command": "go build -o terraform-provider-cachecloud",
"problemMatcher": ["$go"]
},
{
"label": "install-provider-local",
"type": "shell",
"command": "go build -o ~/.terraform.d/plugins/registry.terraform.io/example/cachecloud/99.0.0/darwin_amd64/terraform-provider-cachecloud",
"dependsOn": ["build-provider"]
}
]
}
Debug Mode Provider
Enable Debug Mode
// main.go
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/example/terraform-provider-cachecloud/internal/provider"
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run provider with support for debuggers")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/example/cachecloud",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New, opts)
if err != nil {
log.Fatal(err.Error())
}
}
Using Debug Mode
# Terminal 1: Start provider in debug mode
go run main.go --debug
# Output will show:
# Provider started. To attach Terraform CLI, set the TF_REATTACH_PROVIDERS environment variable with the following:
#
# TF_REATTACH_PROVIDERS='{"registry.terraform.io/example/cachecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":12345,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin123456"}}}'
# Terminal 2: Use the provider
export TF_REATTACH_PROVIDERS='...' # Copy from Terminal 1
terraform plan
Breakpoint Strategies
Setting Breakpoints
// internal/provider/resource_cache.go
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// Set breakpoint here to inspect plan
if resp.Diagnostics.HasError() {
return
}
// Set breakpoint here to inspect API call
cache, err := r.client.CreateCache(ctx, &client.CreateCacheInput{
Name: plan.Name.ValueString(),
NodeType: plan.NodeType.ValueString(),
})
// Set breakpoint here to inspect response
if err != nil {
resp.Diagnostics.AddError("API Error", err.Error())
return
}
// Set breakpoint here to inspect state
state := CacheResourceModel{
ID: types.StringValue(cache.ID),
Name: types.StringValue(cache.Name),
NodeType: types.StringValue(cache.NodeType),
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
Conditional Breakpoints
// In Delve
(dlv) break resource_cache.go:42
(dlv) condition 1 plan.Name.ValueString() == "test-cache"
Function Breakpoints
# Break at function entry
(dlv) break CacheResource.Create
# Break at method
(dlv) break (*CacheResource).Create
# Break at specific line
(dlv) break resource_cache.go:42
Inspection Techniques
Variable Inspection
# Print variable
(dlv) print plan
(dlv) p plan.Name
# Print complex structures
(dlv) print plan
github.com/example/terraform-provider-cachecloud/internal/provider.CacheResourceModel {
ID: github.com/hashicorp/terraform-plugin-framework/types/basetypes.StringValue {...},
Name: github.com/hashicorp/terraform-plugin-framework/types/basetypes.StringValue {
state: 1,
value: "test-cache",
},
...
}
# Pretty print
(dlv) p -fmt json plan
# Print call stack
(dlv) stack
# List goroutines
(dlv) goroutines
# Print all local variables
(dlv) locals
# Print function arguments
(dlv) args
Stepping Through Code
# Step into function
(dlv) step
(dlv) s
# Step over function
(dlv) next
(dlv) n
# Step out of function
(dlv) stepout
# Continue execution
(dlv) continue
(dlv) c
# Restart debugging
(dlv) restart
(dlv) r
Expression Evaluation
# Evaluate expression
(dlv) print len(plan.SubnetIDs.Elements())
# Call function
(dlv) call plan.Name.ValueString()
# Modify variable (careful!)
(dlv) set plan.Name = types.StringValue("modified-name")
Remote Debugging
Setup Remote Debug Server
# On remote server
dlv debug --headless --listen=:2345 --api-version=2 ./main.go
Connect from Local Machine
// .vscode/launch.json
{
"name": "Connect to Remote",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app",
"port": 2345,
"host": "remote-server.example.com"
}
Debugging Common Scenarios
Type Conversion Issues
// Set breakpoint before conversion
var subnets []string
resp.Diagnostics.Append(plan.SubnetIDs.ElementsAs(ctx, &subnets, false)...)
// In debugger, inspect types
(dlv) print plan.SubnetIDs
(dlv) whatis plan.SubnetIDs
Type: github.com/hashicorp/terraform-plugin-framework/types/basetypes.ListValue
// Check for errors
(dlv) print resp.Diagnostics.HasError()
true
// Inspect diagnostics
(dlv) print resp.Diagnostics.Errors()
State Management Issues
// Debug state operations
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CacheResourceModel
// Breakpoint: Inspect incoming state
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
// In debugger
(dlv) print state
(dlv) print state.ID.ValueString()
// Breakpoint: Inspect API response
cache, err := r.client.GetCache(ctx, state.ID.ValueString())
(dlv) print cache
(dlv) print err
// Breakpoint: Inspect new state before setting
newState := CacheResourceModel{...}
(dlv) print newState
(dlv) print newState.Name.Equal(state.Name)
}
API Call Debugging
// Debug HTTP requests
func (c *Client) CreateCache(ctx context.Context, input *CreateCacheInput) (*Cache, error) {
body, err := json.Marshal(input)
// Breakpoint: Inspect request body
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/caches", bytes.NewReader(body))
// Breakpoint: Inspect request
(dlv) print req.Method
(dlv) print req.URL.String()
(dlv) print string(body)
resp, err := c.httpClient.Do(req)
// Breakpoint: Inspect response
(dlv) print resp.StatusCode
(dlv) print resp.Header
if err != nil {
return nil, err
}
}
Performance Debugging
Profiling Integration
import _ "net/http/pprof"
func main() {
// Enable pprof
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... rest of main
}
Access Profiles
# CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Heap profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine
# Interactive analysis
(pprof) top
(pprof) list functionName
(pprof) web
Memory Leak Detection
# Take heap snapshot
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# Wait and take another
sleep 60
curl http://localhost:6060/debug/pprof/heap > heap2.prof
# Compare
go tool pprof -base heap1.prof heap2.prof
(pprof) top
(pprof) list
Debugging Tests
Test-Specific Breakpoints
func TestCacheResource_Create(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "cachecloud_cache" "test" {
name = "debug-test"
node_type = "cache.t3.micro"
}
`,
Check: resource.ComposeTestCheckFunc(
// Set breakpoint in check function
func(s *terraform.State) error {
// Inspect terraform state
rs := s.RootModule().Resources["cachecloud_cache.test"]
// Add debug code here
return nil
},
),
},
},
})
}
Debug Test Execution
# Run single test with debugger
dlv test ./internal/provider -- -test.run TestCacheResource_Create
# In debugger
(dlv) break TestCacheResource_Create
(dlv) continue
(dlv) step
Debug Helpers
Custom Debug Functions
// debug.go
package provider
import (
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// DebugPrint pretty prints any value (use in debugger)
func DebugPrint(v interface{}) {
data, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(data))
}
// DebugStringValue extracts string value safely
func DebugStringValue(v types.String) string {
if v.IsNull() {
return "<null>"
}
if v.IsUnknown() {
return "<unknown>"
}
return v.ValueString()
}
// DebugListElements extracts list elements
func DebugListElements(ctx context.Context, v types.List) []string {
var elements []string
v.ElementsAs(ctx, &elements, false)
return elements
}
Usage in Debugger
# In Delve
(dlv) call DebugPrint(plan)
(dlv) call DebugStringValue(plan.Name)
(dlv) call DebugListElements(ctx, plan.SubnetIDs)
Troubleshooting Debugger Issues
Common Problems
# Problem: Breakpoint not hit
# Solution: Ensure code is not optimized
go build -gcflags="all=-N -l"
# Problem: Cannot inspect variables
# Solution: Disable optimizations
GOFLAGS=-gcflags="all=-N -l" dlv debug
# Problem: Source code not found
# Solution: Ensure correct GOPATH/module cache
(dlv) source /path/to/source
# Problem: Debugger detaches
# Solution: Increase timeout
dlv debug --backend=lldb
Verification Steps
# 1. Verify debugger version
dlv version
# 2. Test simple program
echo 'package main; func main() { println("test") }' > test.go
dlv debug test.go
(dlv) break main.main
(dlv) continue
# 3. Check build flags
go build -x 2>&1 | grep gcflags
Best Practices
1. Strategic Breakpoints
// ✅ Good: At key decision points
if resp.StatusCode != 200 {
// Breakpoint here
return handleError(resp)
}
// ❌ Bad: Inside tight loops
for _, item := range items {
// Don't breakpoint here in production code
process(item)
}
2. Minimal Debug Code
// ✅ Good: Use debugger features
// Set conditional breakpoint in debugger
// ❌ Bad: Litter code with debug statements
fmt.Println("DEBUG: here 1")
// ...
fmt.Println("DEBUG: here 2")
3. Clean Up
// ✅ Good: Remove debug code before commit
// Use linter to catch debug imports
// ❌ Bad: Leave debug artifacts
import _ "net/http/pprof" // Remove if not needed
Key Takeaways
- ✅ Use Delve for powerful debugging
- ✅ Integrate with VSCode for productivity
- ✅ Set strategic breakpoints
- ✅ Master inspection commands
- ✅ Use debug mode for provider testing
- ✅ Profile for performance issues
- ✅ Debug tests systematically
- ✅ Clean up debug code
What’s Next?
Learn about common provider issues and their solutions.
Previous: ← Logging Strategies
Next: Common Issues →
Last updated: July 2, 2026