Debugging Techniques

6 minute read

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.

Next: Common Issues →


Previous: ← Logging Strategies
Next: Common Issues →

Last updated: July 2, 2026

Updated: