Code Monitoring: Debug Production Without Stopping Your Application

Tracekit - code monitoring

Code monitoring revolutionizes how you debug production applications. Instead of stopping your application, redeploying with debug logs, or reproducing issues locally, code monitoring lets you set non-breaking breakpoints in production that capture variable state, stack traces, and request context—all with less than 5ms overhead. This comprehensive guide shows you how to use code monitoring to debug production issues faster, verify calculations, and investigate performance problems without impacting your users.

Table of Contents

What is Code Monitoring?

Code monitoring is a production debugging technique that allows you to set non-breaking breakpoints in your running application. Unlike traditional debuggers that pause execution, code monitoring breakpoints capture variable state, stack traces, and request context without stopping your application or affecting user experience.

Key Capabilities

  • Non-breaking breakpoints – Capture data without stopping execution
  • Variable state capture – See all variable values at the exact moment the breakpoint was hit
  • Full stack traces – Complete call stack showing how code reached that point
  • Request context – HTTP headers, trace IDs, user IDs, and more
  • Automatic code discovery – Code is automatically indexed from your traces
  • Zero downtime – Debug production without redeploying

The Problem with Traditional Production Debugging

Traditional production debugging has significant limitations:

1. Can’t Reproduce Locally

Many production issues only occur with real data, real traffic, or specific user conditions. Reproducing them locally is often impossible.

2. Debug Logs Are Inefficient

Adding debug logs requires:

  • Writing log statements
  • Redeploying your application
  • Waiting for the issue to occur again
  • Searching through logs
  • Removing debug logs after fixing

This process can take hours or days.

3. Traditional Debuggers Stop Execution

Using a traditional debugger in production means:

  • Stopping the application (affecting all users)
  • Connecting to a running process (security risk)
  • Stepping through code manually (time-consuming)
  • Only seeing one request at a time

4. Missing Context

When debugging production issues, you need:

  • What data triggered the issue?
  • What was the user’s state?
  • What was the request context?
  • What were variable values at that exact moment?

Traditional debugging methods don’t capture this context effectively.

How Code Monitoring Works

Code monitoring uses a three-step process:

Step 1: Automatic Code Discovery

Code monitoring automatically indexes your code from traces you’re already sending. When traces contain stack traces (from errors or instrumentation), the system extracts:

  • File paths
  • Function names
  • Line numbers
  • Code structure

No extra instrumentation needed! Your existing distributed tracing setup provides all the information.

Step 2: Set Breakpoints

Once your code is discovered, you can:

  • Browse your code – See all discovered files and functions in the UI
  • Set breakpoints – Click on any line of code to set a breakpoint
  • Configure conditions – Only capture when specific conditions are met (optional)
  • Set sampling rates – Limit captures to avoid performance impact

Step 3: Capture Snapshots

When your code executes and hits a breakpoint:

  • Variables are captured – All local variables, function parameters, and return values
  • Stack trace is recorded – Complete call stack showing execution path
  • Context is preserved – HTTP headers, trace IDs, user IDs, request data
  • Application continues – No interruption to user requests

You can then view these snapshots in the UI to understand exactly what happened.

Key Features of Code Monitoring

1. Non-Breaking Breakpoints

Traditional breakpoints pause execution. Code monitoring breakpoints capture data without stopping:

// Traditional debugger: Execution stops here
// Code monitoring: Captures variables and continues

func processPayment(orderID string, amount float64) {
    // Breakpoint set here - captures orderID, amount, and all variables
    // But execution continues immediately
    payment := chargeCard(orderID, amount)
    // ...
}

2. Automatic Breakpoint Registration

Instead of manually creating breakpoints in the UI, you can use automatic registration:

// Automatic file/line detection + auto-creates breakpoint!
sdk.CheckAndCaptureWithContext(ctx, "payment-processing", map[string]interface{}{
    "userID": userID,
    "amount": amount,
    "orderID": orderID,
})

// The SDK automatically:
// 1. Detects file path and line number
// 2. Creates/updates breakpoint in TraceKit
// 3. Captures snapshot when breakpoint is active

3. Variable State Capture

See exactly what values variables had when the breakpoint was hit:

  • Function parameters
  • Local variables
  • Return values
  • Struct fields
  • Map/slice contents

4. Request Context

Every snapshot includes full request context:

  • HTTP method and URL
  • Request headers
  • Query parameters
  • Trace ID (links to distributed trace)
  • User ID and session data
  • Request body (if applicable)

Code Monitoring in Go

Go is one of the best-supported languages for code monitoring. Here’s how to get started:

Step 1: Install TraceKit Go SDK

go get github.com/Tracekit-Dev/go-sdk/tracekit

Step 2: Initialize SDK with Code Monitoring

package main

import (
    "context"
    "os"
    
    "github.com/Tracekit-Dev/go-sdk/tracekit"
)

func main() {
    // Initialize SDK with code monitoring enabled
    sdk, err := tracekit.NewSDK(&tracekit.Config{
        APIKey:               os.Getenv("TRACEKIT_API_KEY"),
        ServiceName:          "my-service",
        EnableCodeMonitoring: true,  // Enable code monitoring
    })
    if err != nil {
        log.Fatal(err)
    }
    defer sdk.Shutdown(context.Background())
    
    // Your application code...
}

Step 3: Add Automatic Breakpoints

The easiest way to use code monitoring is with automatic breakpoint registration:

func processOrder(ctx context.Context, orderID string, userID int) error {
    // Automatic breakpoint registration
    // SDK detects file path and line number automatically
    sdk.CheckAndCaptureWithContext(ctx, "process-order", map[string]interface{}{
        "orderID": orderID,
        "userID":  userID,
        "step":    "starting",
    })
    
    // Your business logic
    order, err := fetchOrder(orderID)
    if err != nil {
        return err
    }
    
    // Another checkpoint
    sdk.CheckAndCaptureWithContext(ctx, "validate-order", map[string]interface{}{
        "orderID":    orderID,
        "orderTotal": order.Total,
        "step":       "validation",
    })
    
    // Process payment
    payment, err := chargePayment(order)
    if err != nil {
        // Capture error state
        sdk.CheckAndCaptureWithContext(ctx, "payment-failed", map[string]interface{}{
            "orderID": orderID,
            "error":   err.Error(),
            "step":    "payment",
        })
        return err
    }
    
    return nil
}

Step 4: View Snapshots

When your code executes and hits these checkpoints:

  1. Go to Code Monitoring → Snapshots in the TraceKit UI
  2. See all captured snapshots with variable values
  3. Click on any snapshot to see:
    • All variable values at that point
    • Complete stack trace
    • Request context (headers, trace ID, user ID)
    • Link to full distributed trace

Real-World Go Example: Debugging Payment Processing

func ProcessPayment(ctx context.Context, orderID string, amount float64) (*Payment, error) {
    // Set checkpoint at payment start
    sdk.CheckAndCaptureWithContext(ctx, "payment-start", map[string]interface{}{
        "orderID": orderID,
        "amount":  amount,
    })
    
    // Validate amount
    if amount <= 0 {
        sdk.CheckAndCaptureWithContext(ctx, "invalid-amount", map[string]interface{}{
            "orderID": orderID,
            "amount":  amount,
            "error":   "amount must be positive",
        })
        return nil, fmt.Errorf("invalid amount: %f", amount)
    }
    
    // Fetch payment method
    paymentMethod, err := getPaymentMethod(ctx, orderID)
    if err != nil {
        sdk.CheckAndCaptureWithContext(ctx, "payment-method-error", map[string]interface{}{
            "orderID": orderID,
            "error":   err.Error(),
        })
        return nil, err
    }
    
    // Charge card
    result, err := chargeCard(ctx, paymentMethod, amount)
    if err != nil {
        // This is where you'd want to debug - what went wrong?
        sdk.CheckAndCaptureWithContext(ctx, "charge-failed", map[string]interface{}{
            "orderID":       orderID,
            "amount":        amount,
            "paymentMethod": paymentMethod.ID,
            "error":         err.Error(),
        })
        return nil, err
    }
    
    return result, nil
}

When a payment fails in production, you can see:

  • Exact amount that failed
  • Payment method details
  • Error message
  • Full request context
  • Complete call stack

All without stopping your application or redeploying!

Code Monitoring in PHP/Laravel

PHP and Laravel applications can also use code monitoring. Here's a quick example:

use TraceKit\Laravel\TracekitClient;

public function processOrder(Order $order, TracekitClient $tracekit)
{
    // Set checkpoint
    $tracekit->checkAndCapture('process-order', [
        'order_id' => $order->id,
        'user_id' => $order->user_id,
        'total' => $order->total,
    ]);
    
    try {
        // Your business logic
        $payment = $this->chargePayment($order);
        
        $tracekit->checkAndCapture('payment-success', [
            'order_id' => $order->id,
            'payment_id' => $payment->id,
        ]);
        
        return $payment;
    } catch (\Exception $e) {
        // Capture error state
        $tracekit->checkAndCapture('payment-error', [
            'order_id' => $order->id,
            'error' => $e->getMessage(),
            'stack_trace' => $e->getTraceAsString(),
        ]);
        
        throw $e;
    }
}

Code Monitoring in Node.js

Node.js applications can use code monitoring similarly:

const { TraceKit } = require('@tracekit/node');

async function processOrder(orderId, userId) {
    // Set checkpoint
    TraceKit.checkAndCapture('process-order', {
        orderId: orderId,
        userId: userId,
        step: 'starting',
    });
    
    try {
        const order = await fetchOrder(orderId);
        
        TraceKit.checkAndCapture('order-fetched', {
            orderId: orderId,
            orderTotal: order.total,
            step: 'validation',
        });
        
        const payment = await chargePayment(order);
        
        return payment;
    } catch (error) {
        TraceKit.checkAndCapture('order-error', {
            orderId: orderId,
            error: error.message,
            stack: error.stack,
        });
        
        throw error;
    }
}

Real-World Use Cases

1. Debug Production Issues

Scenario: A customer reports an error, but you can't reproduce it locally.

Solution: Set a breakpoint at the error location. The next time it happens, you'll see:

  • Exact user data that triggered it
  • Variable values at the moment of failure
  • Complete request context
  • Call stack showing how code reached that point

2. Verify Calculations

Scenario: You need to verify that financial calculations are correct in production.

func calculateTotal(items []Item, taxRate float64) float64 {
    // Set breakpoint to verify calculation
    sdk.CheckAndCaptureWithContext(ctx, "calculate-total", map[string]interface{}{
        "itemCount": len(items),
        "taxRate":   taxRate,
        "items":     items, // Full item details captured
    })
    
    subtotal := 0.0
    for _, item := range items {
        subtotal += item.Price * float64(item.Quantity)
    }
    
    total := subtotal * (1 + taxRate)
    
    // Capture result
    sdk.CheckAndCaptureWithContext(ctx, "total-calculated", map[string]interface{}{
        "subtotal": subtotal,
        "tax":      subtotal * taxRate,
        "total":    total,
    })
    
    return total
}

You can now verify calculations are correct by inspecting captured snapshots.

3. Performance Investigation

Scenario: Some requests are slow, but you don't know why.

Solution: Set breakpoints at key operations and capture:

  • Input sizes (array lengths, data volumes)
  • Timing information
  • Which code paths are taken

Compare snapshots from fast vs slow requests to identify patterns.

4. Track Data Flow

Scenario: You need to understand how data flows through complex pipelines.

Solution: Set breakpoints at each stage of the pipeline:

func processPipeline(ctx context.Context, data Data) Result {
    // Stage 1: Validation
    sdk.CheckAndCaptureWithContext(ctx, "stage-validation", map[string]interface{}{
        "data": data,
    })
    validated := validate(data)
    
    // Stage 2: Transformation
    sdk.CheckAndCaptureWithContext(ctx, "stage-transform", map[string]interface{}{
        "validated": validated,
    })
    transformed := transform(validated)
    
    // Stage 3: Processing
    sdk.CheckAndCaptureWithContext(ctx, "stage-process", map[string]interface{}{
        "transformed": transformed,
    })
    result := process(transformed)
    
    return result
}

You can now see exactly how data changes at each stage.

Best Practices

1. Use Descriptive Checkpoint Names

Use clear, descriptive names that indicate what's happening:

// Good
sdk.CheckAndCaptureWithContext(ctx, "payment-processing-started", ...)
sdk.CheckAndCaptureWithContext(ctx, "payment-validation-failed", ...)

// Bad
sdk.CheckAndCaptureWithContext(ctx, "checkpoint1", ...)
sdk.CheckAndCaptureWithContext(ctx, "debug", ...)

2. Include Relevant Context

Capture variables that help you understand the state:

sdk.CheckAndCaptureWithContext(ctx, "order-processing", map[string]interface{}{
    "orderID":    orderID,      // Always include IDs
    "userID":     userID,       // User context
    "orderTotal": order.Total,  // Key business data
    "step":       "validation", // Current step
})

3. Use Conditions for High-Traffic Code

For frequently executed code, use conditions to limit captures:

// Only capture if order total > $1000
if order.Total > 1000 {
    sdk.CheckAndCaptureWithContext(ctx, "high-value-order", map[string]interface{}{
        "orderID": orderID,
        "total":   order.Total,
    })
}

4. Clean Up After Debugging

Once you've fixed the issue, disable or remove breakpoints to avoid unnecessary overhead.

5. Link to Distributed Traces

Code monitoring snapshots automatically link to distributed traces. Use this to see the full request journey.

Performance Considerations

Overhead is Minimal

Code monitoring has less than 5ms overhead per checkpoint when active. When breakpoints are disabled, overhead is near zero.

Use Sampling

For high-traffic endpoints, use sampling:

// Only capture 10% of requests
if rand.Float64() < 0.1 {
    sdk.CheckAndCaptureWithContext(ctx, "high-traffic-endpoint", ...)
}

Set Expiration Times

Configure breakpoints to expire after a set time to avoid leaving them active indefinitely.

Limit Capture Frequency

Use max_captures and capture_frequency settings to limit total captures per breakpoint.

Frequently Asked Questions (FAQ)

What is code monitoring?

Code monitoring is a production debugging technique that allows you to set non-breaking breakpoints in your running application. Unlike traditional debuggers that pause execution, code monitoring breakpoints capture variable state, stack traces, and request context without stopping your application or affecting user experience. It's perfect for debugging production issues that can't be reproduced locally.

How does code monitoring work?

Code monitoring works in three steps: 1) Your code is automatically discovered and indexed from distributed traces you're already sending, 2) You set breakpoints on specific lines of code in the UI or using automatic registration, 3) When code executes and hits a breakpoint, variables, stack traces, and context are captured without stopping execution. You can then view these snapshots to understand exactly what happened.

Does code monitoring slow down my application?

No, code monitoring has minimal overhead—less than 5ms per checkpoint when active. When breakpoints are disabled, overhead is near zero. You can use sampling, expiration times, and capture frequency limits to further reduce impact on high-traffic endpoints.

Can I use code monitoring in production?

Yes, code monitoring is designed for production use. Non-breaking breakpoints don't stop execution, so they don't affect user experience. The low overhead (< 5ms) makes it safe for production environments. You can enable/disable breakpoints without redeploying your application.

What languages support code monitoring?

Code monitoring currently supports Go, PHP, Laravel, and Node.js, with Python and more languages coming soon. The feature works with any language that supports distributed tracing, as code discovery happens automatically from stack traces in your traces.

How is code monitoring different from logging?

Code monitoring captures complete variable state, stack traces, and context automatically at specific code locations without requiring you to write log statements. Logging requires you to manually add log statements, redeploy, wait for issues to occur, then search through logs. Code monitoring lets you set breakpoints instantly and see captured data immediately, without code changes or redeployments.

Do I need to instrument my code for code monitoring?

Code discovery happens automatically from your existing distributed traces—no extra instrumentation needed. However, for automatic breakpoint registration, you can use CheckAndCaptureWithContext() calls in your code, which the SDK uses to automatically detect file paths and line numbers and create breakpoints for you.

Next Steps

Ready to start debugging production without downtime?

  • Enable code monitoring in your TraceKit SDK configuration
  • Browse your discovered code in the Code Monitoring page
  • Set your first breakpoint on a line you want to debug
  • Use automatic registration with CheckAndCaptureWithContext() for easier setup
  • Try TraceKit - Code monitoring included with distributed tracing

Code monitoring transforms production debugging from days of guesswork to minutes of certainty. Start debugging production issues without stopping your application.


Questions about code monitoring? Start a free trial and see code monitoring in action. Check our code monitoring documentation for language-specific guides.

About Terry Osayawe

Founder of TraceKit. On a mission to make production debugging effortless.

Ready to Debug 10x Faster?

Join teams who stopped guessing and started knowing

Start Free
Start Free

Free forever tier • No credit card required