Logging Strategies
Logging Overview
Effective logging is crucial for understanding provider behavior, debugging issues, and maintaining production systems. The Plugin Framework uses tflog for structured, context-aware logging.
Log Levels
Available Levels
import "github.com/hashicorp/terraform-plugin-log/tflog"
// TRACE: Very detailed information
tflog.Trace(ctx, "Entering function")
// DEBUG: Detailed debugging information
tflog.Debug(ctx, "Cache configuration", map[string]interface{}{
"name": "test-cache",
"type": "redis",
})
// INFO: Informational messages
tflog.Info(ctx, "Cache created successfully")
// WARN: Warning messages
tflog.Warn(ctx, "Using deprecated configuration")
// ERROR: Error messages
tflog.Error(ctx, "Failed to create cache", map[string]interface{}{
"error": err.Error(),
})
When to Use Each Level
// TRACE: Function entry/exit, detailed flow
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Trace(ctx, "CacheResource.Create started")
defer tflog.Trace(ctx, "CacheResource.Create completed")
// Implementation...
}
// DEBUG: Important values, decision points
tflog.Debug(ctx, "Validating cache configuration", map[string]interface{}{
"name": plan.Name.ValueString(),
"node_type": plan.NodeType.ValueString(),
})
// INFO: Significant operations
tflog.Info(ctx, "Cache creation initiated", map[string]interface{}{
"cache_id": cacheID,
})
// WARN: Deprecated features, suboptimal configs
if config.LegacyMode.ValueBool() {
tflog.Warn(ctx, "Legacy mode is deprecated and will be removed in v3.0")
}
// ERROR: Operation failures
if err != nil {
tflog.Error(ctx, "API request failed", map[string]interface{}{
"operation": "CreateCache",
"error": err.Error(),
})
}
Structured Logging
Basic Structure
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
tflog.Debug(ctx, "Creating cache", map[string]interface{}{
"name": plan.Name.ValueString(),
"node_type": plan.NodeType.ValueString(),
"engine": plan.Engine.ValueString(),
"engine_version": plan.EngineVersion.ValueString(),
})
// Create cache
cache, err := r.client.CreateCache(ctx, &client.CreateCacheInput{
Name: plan.Name.ValueString(),
NodeType: plan.NodeType.ValueString(),
Engine: plan.Engine.ValueString(),
EngineVersion: plan.EngineVersion.ValueString(),
})
if err != nil {
tflog.Error(ctx, "Failed to create cache", map[string]interface{}{
"name": plan.Name.ValueString(),
"error": err.Error(),
})
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Failed to create cache: %s", err))
return
}
tflog.Info(ctx, "Cache created successfully", map[string]interface{}{
"cache_id": cache.ID,
"name": cache.Name,
})
}
Complex Objects
func logNetworkConfig(ctx context.Context, config *NetworkConfigModel) {
var subnets []string
config.SubnetIDs.ElementsAs(ctx, &subnets, false)
var securityGroups []string
config.SecurityGroupIDs.ElementsAs(ctx, &securityGroups, false)
tflog.Debug(ctx, "Network configuration", map[string]interface{}{
"vpc_id": config.VPCID.ValueString(),
"subnet_ids": subnets,
"security_groups": securityGroups,
"subnet_count": len(subnets),
"security_group_count": len(securityGroups),
})
}
Context-Aware Logging
Subsystem Logging
// Create subsystem logger
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Add subsystem to context
ctx = tflog.SetField(ctx, "resource", "cachecloud_cache")
ctx = tflog.SetField(ctx, "operation", "read")
tflog.Debug(ctx, "Reading cache state")
// Logs will include subsystem fields automatically
}
Request ID Tracking
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Generate request ID
requestID := uuid.New().String()
ctx = tflog.SetField(ctx, "request_id", requestID)
tflog.Info(ctx, "Starting cache creation")
// All subsequent logs will include request_id
result, err := r.client.CreateCache(ctx, input)
if err != nil {
tflog.Error(ctx, "Creation failed")
// Error log includes request_id
return
}
tflog.Info(ctx, "Creation completed")
// Success log includes request_id
}
User Identity
func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config providerModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Add provider-level context
ctx = tflog.SetField(ctx, "provider_version", "2.0.0")
ctx = tflog.SetField(ctx, "api_endpoint", config.Endpoint.ValueString())
// Verify credentials and add user info
user, err := verifyCredentials(ctx, config.APIKey.ValueString())
if err == nil {
ctx = tflog.SetField(ctx, "user_id", user.ID)
ctx = tflog.SetField(ctx, "organization", user.Organization)
}
tflog.Info(ctx, "Provider configured")
}
Sensitive Data Handling
Redaction
// Mask sensitive values
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// Create masked logger for sensitive fields
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "password")
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "api_key")
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "auth_token")
tflog.Debug(ctx, "Creating cache with auth", map[string]interface{}{
"name": plan.Name.ValueString(),
"password": plan.Password.ValueString(), // Will be masked
})
}
Manual Masking
func maskSensitiveString(s string) string {
if len(s) <= 4 {
return "***"
}
return s[:2] + "***" + s[len(s)-2:]
}
tflog.Debug(ctx, "API key configured", map[string]interface{}{
"api_key": maskSensitiveString(apiKey),
})
Performance Logging
Operation Timing
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
start := time.Now()
defer func() {
duration := time.Since(start)
tflog.Debug(ctx, "Read operation completed", map[string]interface{}{
"duration_ms": duration.Milliseconds(),
})
}()
// Implementation...
}
API Call Metrics
type MetricsLogger struct {
mu sync.RWMutex
operationCounts map[string]int64
operationTimes map[string][]time.Duration
}
func (m *MetricsLogger) LogAPICall(ctx context.Context, operation string, duration time.Duration, err error) {
m.mu.Lock()
m.operationCounts[operation]++
m.operationTimes[operation] = append(m.operationTimes[operation], duration)
m.mu.Unlock()
tflog.Debug(ctx, "API call completed", map[string]interface{}{
"operation": operation,
"duration_ms": duration.Milliseconds(),
"success": err == nil,
})
}
func (m *MetricsLogger) LogSummary(ctx context.Context) {
m.mu.RLock()
defer m.mu.RUnlock()
tflog.Info(ctx, "Operation summary", map[string]interface{}{
"total_operations": len(m.operationCounts),
"operations": m.operationCounts,
})
}
Client Logging
HTTP Request Logging
type LoggingTransport struct {
next http.RoundTripper
}
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
start := time.Now()
// Log request
tflog.Trace(ctx, "HTTP request", map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"headers": sanitizeHeaders(req.Header),
})
resp, err := t.next.RoundTrip(req)
duration := time.Since(start)
if err != nil {
tflog.Error(ctx, "HTTP request failed", map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
})
return nil, err
}
tflog.Debug(ctx, "HTTP response", map[string]interface{}{
"method": req.Method,
"url": req.URL.String(),
"status": resp.StatusCode,
"duration_ms": duration.Milliseconds(),
})
return resp, nil
}
func sanitizeHeaders(headers http.Header) map[string]string {
sanitized := make(map[string]string)
for key, values := range headers {
if strings.ToLower(key) == "authorization" {
sanitized[key] = "***REDACTED***"
} else {
sanitized[key] = strings.Join(values, ", ")
}
}
return sanitized
}
Error Logging
Comprehensive Error Context
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state CacheResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
tflog.Error(ctx, "Failed to retrieve state", map[string]interface{}{
"diagnostics": diagnosticsToStrings(resp.Diagnostics),
})
return
}
// Update operation
updated, err := r.client.UpdateCache(ctx, state.ID.ValueString(), &client.UpdateCacheInput{
Name: plan.Name.ValueString(),
NodeType: plan.NodeType.ValueString(),
})
if err != nil {
// Log detailed error information
tflog.Error(ctx, "Update operation failed", map[string]interface{}{
"cache_id": state.ID.ValueString(),
"old_name": state.Name.ValueString(),
"new_name": plan.Name.ValueString(),
"error": err.Error(),
"error_type": fmt.Sprintf("%T", err),
})
// Check for specific error types
if apiErr, ok := err.(*client.APIError); ok {
tflog.Error(ctx, "API error details", map[string]interface{}{
"status_code": apiErr.StatusCode,
"error_code": apiErr.Code,
"message": apiErr.Message,
})
}
resp.Diagnostics.AddError("Update Failed", fmt.Sprintf("Failed to update cache: %s", err))
return
}
tflog.Info(ctx, "Cache updated successfully", map[string]interface{}{
"cache_id": updated.ID,
"name": updated.Name,
})
}
func diagnosticsToStrings(diags diag.Diagnostics) []string {
var result []string
for _, d := range diags {
result = append(result, fmt.Sprintf("%s: %s", d.Summary(), d.Detail()))
}
return result
}
Log Configuration
Environment Variables
# Set log level
export TF_LOG=TRACE
export TF_LOG=DEBUG
export TF_LOG=INFO
export TF_LOG=WARN
export TF_LOG=ERROR
# Provider-specific logging
export TF_LOG_PROVIDER=TRACE
# Save logs to file
export TF_LOG_PATH=terraform.log
Conditional Logging
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Only log verbose details in TRACE mode
if os.Getenv("TF_LOG") == "TRACE" {
tflog.Trace(ctx, "Detailed state information", map[string]interface{}{
"full_state": fmt.Sprintf("%+v", state),
})
}
// Always log important information
tflog.Info(ctx, "Reading cache", map[string]interface{}{
"cache_id": state.ID.ValueString(),
})
}
Testing with Logs
Capture Logs in Tests
func TestCacheResource_Create(t *testing.T) {
var logOutput strings.Builder
// Create context with log sink
ctx := tflogtest.RootLogger(context.Background(), &logOutput)
// Run test
r := NewCacheResource()
// ... test logic ...
// Verify logs
logs := logOutput.String()
assert.Contains(t, logs, "Creating cache")
assert.Contains(t, logs, "Cache created successfully")
}
Verify Log Levels
func TestLoggingLevels(t *testing.T) {
tests := []struct {
level string
contains []string
excludes []string
}{
{
level: "ERROR",
contains: []string{"ERROR"},
excludes: []string{"DEBUG", "INFO"},
},
{
level: "DEBUG",
contains: []string{"DEBUG", "INFO", "ERROR"},
excludes: []string{"TRACE"},
},
}
for _, tt := range tests {
t.Run(tt.level, func(t *testing.T) {
os.Setenv("TF_LOG", tt.level)
// Run operation and check logs
})
}
}
Best Practices
1. Consistent Structure
// ✅ Good: Consistent key names
tflog.Debug(ctx, "Operation started", map[string]interface{}{
"resource_id": id,
"resource_name": name,
"operation": "create",
})
// ❌ Bad: Inconsistent keys
tflog.Debug(ctx, "Op start", map[string]interface{}{
"id": id,
"Name": name,
"op": "create",
})
2. Meaningful Messages
// ✅ Good: Clear, descriptive message
tflog.Info(ctx, "Cache cluster scaling initiated", map[string]interface{}{
"cluster_id": clusterID,
"current_nodes": currentCount,
"target_nodes": targetCount,
})
// ❌ Bad: Vague message
tflog.Info(ctx, "Scaling", map[string]interface{}{
"id": clusterID,
})
3. Appropriate Level
// ✅ Good: Appropriate levels
tflog.Trace(ctx, "Function entry")
tflog.Debug(ctx, "Processing configuration")
tflog.Info(ctx, "Operation completed")
tflog.Error(ctx, "Operation failed")
// ❌ Bad: Everything at same level
tflog.Info(ctx, "Function entry")
tflog.Info(ctx, "Processing")
tflog.Info(ctx, "Error occurred")
Key Takeaways
- ✅ Use tflog for structured logging
- ✅ Choose appropriate log levels
- ✅ Add context fields for tracking
- ✅ Redact sensitive information
- ✅ Log performance metrics
- ✅ Provide comprehensive error context
- ✅ Test logging behavior
- ✅ Follow consistent conventions
What’s Next?
Learn debugging techniques with Delve and VSCode.
Previous: ← Debugging & Troubleshooting
Next: Debugging Techniques →
Last updated: July 2, 2026