Skip to content

OpenTelemetry Tracing ​

Mindwave provides production-grade OpenTelemetry tracing for all LLM operations using GenAI semantic conventions. Get deep observability into your AI applications with automatic instrumentation, cost tracking, and performance monitoring.

Overview ​

What is OpenTelemetry Tracing? ​

OpenTelemetry is an open-source observability framework for cloud-native software. Tracing helps you understand what happens during a request's lifecycle by recording the path through your system as a series of spans organized in a trace.

For LLM applications, tracing is critical for:

  • Production Debugging - Identify slow or failing LLM calls in production
  • Cost Analysis - Track spending by user, feature, or time period
  • Performance Optimization - Find bottlenecks and optimize token usage
  • Quality Monitoring - Detect errors and analyze finish reasons
  • Audit Trails - Understand what prompts were sent and what responses were generated

Why Use Mindwave's Tracing? ​

Automatic Instrumentation

  • Zero-code tracing for all LLM operations
  • Follows OpenTelemetry GenAI semantic conventions
  • Works with any OTLP-compatible backend

LLM-Specific Features

  • Token usage tracking (input, output, cache)
  • Automatic cost estimation with configurable pricing
  • Prompt and completion capture (opt-in, PII-aware)
  • Streaming support with real-time events
  • Multi-provider support (OpenAI, Anthropic, Mistral, Google)

Flexible Storage

  • Database storage for local querying with Eloquent
  • OTLP export to Jaeger, Grafana Tempo, Honeycomb, Datadog
  • Multi-exporter support (use both simultaneously)

Privacy & Security

  • PII redaction by default
  • Configurable message capture
  • Sampling strategies for high-volume applications

Getting Started ​

Installation ​

Mindwave's tracing is included in the package. Simply publish the configuration and migrations:

bash
# Publish configuration
php artisan vendor:publish --tag=mindwave-config

# Publish and run migrations
php artisan vendor:publish --tag=mindwave-migrations
php artisan migrate

This creates two tables:

  • mindwave_traces - One row per trace (conversation/request)
  • mindwave_spans - Individual operations (LLM calls, tool executions)

Basic Configuration ​

Configure tracing in your .env file:

dotenv
# Enable tracing
MINDWAVE_TRACING_ENABLED=true

# Service name (appears in tracing UI)
MINDWAVE_SERVICE_NAME=my-ai-app

# Database storage (enabled by default)
MINDWAVE_TRACE_DATABASE=true

# OTLP export (disabled by default)
MINDWAVE_TRACE_OTLP_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

# Sampling (1.0 = 100% of traces)
MINDWAVE_TRACE_SAMPLER=always_on
MINDWAVE_TRACE_SAMPLE_RATIO=1.0

# Privacy
MINDWAVE_TRACE_CAPTURE_MESSAGES=false  # Don't capture prompts/completions by default

Quick Start Example ​

php
use Mindwave\Mindwave\Facades\Mindwave;

// This LLM call is automatically traced!
$response = Mindwave::llm()
    ->generateText('What is Laravel?');

// Behind the scenes, Mindwave automatically:
// 1. Creates a trace with unique trace_id
// 2. Creates a span for the LLM call
// 3. Records request parameters (model, temperature, etc.)
// 4. Tracks token usage (input, output, total)
// 5. Estimates cost based on provider pricing
// 6. Stores everything in the database

That's it! No additional code required. Every LLM call is now fully traced.

Automatic Instrumentation ​

What Gets Traced Automatically ​

Mindwave automatically instruments:

  • Chat Completions - OpenAI chat, Anthropic messages, etc.
  • Text Completions - Traditional completion endpoints
  • Embeddings - Vector generation for RAG
  • Streaming Responses - Real-time token streaming with events
  • Tool Executions - Function calling operations

Every LLM operation captures:

AttributeDescriptionExample
Trace IDUnique identifier for the operation3c8f4b9a2e1d6c7b8a9e0f1d2c3b4a5e
Span IDUnique identifier for this specific call7b8a9e0f1d2c3b4a
ProviderLLM provider nameopenai, anthropic, mistral
ModelModel identifiergpt-4, claude-3-opus
OperationType of operationchat, text_completion, embeddings
Start/End TimeTimestamps in nanoseconds1700000000000000000
DurationExecution time in nanoseconds1234567890
Token UsageInput, output, cache tokens150 input, 300 output
CostEstimated cost in USD$0.0045
StatusSuccess or error statusok, error
TemperatureModel temperature setting0.7
Max TokensToken limit1000
Finish ReasonsWhy generation stopped["stop"], ["length"]

Trace Structure ​

A trace represents a complete operation (like handling an API request) and contains multiple spans:

mermaid
graph TD
    Trace[Trace: 3c8f4b9a2e1d6c7b8a9e0f1d2c3b4a5e]

    Trace --> Span1[Span: chat gpt-4 client<br/>750ms, $0.0045]
    Trace --> Span2[Span: embeddings text-embedding-ada-002 client<br/>120ms, $0.0001]
    Trace --> Span3[Span: execute_tool web_search internal<br/>450ms]

    Span1 -.-> S1A[operation: chat<br/>provider: openai<br/>model: gpt-4<br/>input_tokens: 150<br/>output_tokens: 300<br/>status: ok]

    Span2 -.-> S2A[operation: embeddings<br/>provider: openai<br/>model: text-embedding-ada-002<br/>input_tokens: 50<br/>status: ok]

    Span3 -.-> S3A[operation: execute_tool<br/>tool_name: web_search<br/>status: ok]

    style Trace fill:#e1f5ff
    style Span1 fill:#fff4e6
    style Span2 fill:#fff4e6
    style Span3 fill:#fff4e6
    style S1A fill:#f5f5f5
    style S2A fill:#f5f5f5
    style S3A fill:#f5f5f5

Example: Viewing Automatic Traces ​

php
use Mindwave\Mindwave\Observability\Models\Trace;
use Mindwave\Mindwave\Observability\Models\Span;

// Make some LLM calls
$response1 = Mindwave::llm('openai')->generateText('Hello');
$response2 = Mindwave::llm('anthropic')->generateText('World');

// Query recent traces
$traces = Trace::with('spans')
    ->orderBy('start_time', 'desc')
    ->limit(10)
    ->get();

foreach ($traces as $trace) {
    echo "Trace: {$trace->trace_id}\n";
    echo "Duration: {$trace->getDurationInMilliseconds()}ms\n";
    echo "Cost: \${$trace->estimated_cost}\n";
    echo "Spans: {$trace->spans->count()}\n\n";

    foreach ($trace->spans as $span) {
        echo "  → {$span->name} ({$span->provider_name})\n";
        echo "    Tokens: {$span->input_tokens} in, {$span->output_tokens} out\n";
        echo "    Duration: {$span->getDurationInMilliseconds()}ms\n";
    }
}

Querying Traces ​

Mindwave provides Eloquent models for querying traces directly from your database.

Find Recent LLM Calls ​

php
use Mindwave\Mindwave\Observability\Models\Trace;

// Get the last 10 LLM calls
$traces = Trace::with('spans')
    ->orderBy('start_time', 'desc')
    ->limit(10)
    ->get();

foreach ($traces as $trace) {
    echo "Trace ID: {$trace->trace_id}\n";
    echo "Duration: {$trace->getDurationInMilliseconds()}ms\n";
    echo "Spans: {$trace->spans->count()}\n\n";
}

Find Expensive Queries ​

php
use Mindwave\Mindwave\Observability\Models\Span;

// Find LLM calls that cost more than $0.10
$expensive = Span::where('input_tokens', '>', 0)
    ->whereNotNull('provider_name')
    ->orderByRaw('(input_tokens + output_tokens) DESC')
    ->with('trace')
    ->limit(20)
    ->get();

foreach ($expensive as $span) {
    $totalTokens = $span->input_tokens + $span->output_tokens;

    echo "Model: {$span->request_model}\n";
    echo "Tokens: {$totalTokens}\n";
    echo "Duration: {$span->getDurationInMilliseconds()}ms\n\n";
}

Find Slow Requests ​

php
// Find requests that took longer than 5 seconds
$slow = Span::slow(5000)->get(); // 5000ms = 5 seconds

foreach ($slow as $span) {
    echo "Model: {$span->request_model}\n";
    echo "Duration: {$span->getDurationInMilliseconds()}ms\n";
    echo "Provider: {$span->provider_name}\n\n";
}

Group by Provider ​

php
// Get usage breakdown by provider
$costByProvider = Span::selectRaw('
        provider_name,
        COUNT(*) as call_count,
        SUM(input_tokens + output_tokens) as total_tokens,
        AVG(duration) as avg_duration_ns
    ')
    ->whereNotNull('provider_name')
    ->groupBy('provider_name')
    ->get();

foreach ($costByProvider as $provider) {
    $avgMs = round($provider->avg_duration_ns / 1_000_000, 2);

    echo "{$provider->provider_name}:\n";
    echo "  Calls: {$provider->call_count}\n";
    echo "  Tokens: " . number_format($provider->total_tokens) . "\n";
    echo "  Avg Duration: {$avgMs}ms\n\n";
}

Group by Model ​

php
// Get usage breakdown by model
$usageByModel = Span::selectRaw('
        request_model,
        COUNT(*) as usage_count,
        SUM(input_tokens) as total_input,
        SUM(output_tokens) as total_output,
        AVG(duration) as avg_duration
    ')
    ->whereNotNull('request_model')
    ->groupBy('request_model')
    ->orderByDesc('usage_count')
    ->get();

foreach ($usageByModel as $model) {
    $avgMs = round($model->avg_duration / 1_000_000, 2);
    $totalTokens = $model->total_input + $model->total_output;

    echo "{$model->request_model}:\n";
    echo "  Uses: {$model->usage_count}\n";
    echo "  Tokens: " . number_format($totalTokens) . "\n";
    echo "  Avg Duration: {$avgMs}ms\n\n";
}

Filter by Date Range ​

php
use Carbon\Carbon;

// Get traces from today
$today = Trace::whereDate('created_at', today())->get();

// Get traces from last 7 days
$lastWeek = Trace::where('created_at', '>=', now()->subDays(7))->get();

// Get traces from specific month
$january = Trace::whereMonth('created_at', 1)
    ->whereYear('created_at', 2025)
    ->get();

Scope Queries ​

The Span model provides convenient query scopes:

php
// Find spans by operation
$chatSpans = Span::operation('chat')->get();
$embeddingsSpans = Span::operation('embeddings')->get();

// Find spans by provider
$openaiSpans = Span::provider('openai')->get();
$anthropicSpans = Span::provider('anthropic')->get();

// Find spans by model
$gpt4Spans = Span::model('gpt-4')->get();

// Find slow spans (default: > 5000ms)
$slowSpans = Span::slow()->get();
$verySlow = Span::slow(10000)->get(); // > 10 seconds

// Find spans with errors
$errors = Span::withErrors()->get();

// Combine scopes
$slowOpenAI = Span::provider('openai')
    ->slow(3000)
    ->orderBy('duration', 'desc')
    ->get();

Cost Analysis ​

Track and analyze your LLM spending with automatic cost estimation.

Daily Spending Report ​

php
use Mindwave\Mindwave\Observability\Models\Trace;
use Carbon\Carbon;

$today = Carbon::today();

// Calculate today's cost by summing trace costs
$todayCost = Trace::whereDate('created_at', $today)
    ->sum('estimated_cost');

echo "Today's LLM spend: \$" . number_format($todayCost, 4) . "\n";

// Get breakdown by provider
$breakdown = Span::whereDate('created_at', $today)
    ->whereNotNull('provider_name')
    ->selectRaw('provider_name, SUM(input_tokens) as input, SUM(output_tokens) as output')
    ->groupBy('provider_name')
    ->get();

foreach ($breakdown as $row) {
    echo "{$row->provider_name}: {$row->input} in, {$row->output} out\n";
}

Monthly Cost Breakdown ​

php
$thisMonth = Trace::whereMonth('created_at', now()->month)
    ->whereYear('created_at', now()->year)
    ->selectRaw('
        DATE(created_at) as date,
        SUM(estimated_cost) as daily_cost,
        SUM(total_input_tokens + total_output_tokens) as daily_tokens,
        COUNT(*) as daily_traces
    ')
    ->groupBy('date')
    ->orderBy('date')
    ->get();

foreach ($thisMonth as $day) {
    echo "{$day->date}: \${$day->daily_cost} ";
    echo "({$day->daily_traces} traces, ";
    echo number_format($day->daily_tokens) . " tokens)\n";
}

Cost Comparison Between Providers ​

php
$comparison = Span::selectRaw('
        provider_name,
        COUNT(*) as calls,
        SUM(input_tokens + output_tokens) as total_tokens
    ')
    ->whereIn('provider_name', ['openai', 'anthropic', 'mistral'])
    ->whereNotNull('provider_name')
    ->groupBy('provider_name')
    ->get();

foreach ($comparison as $provider) {
    echo "{$provider->provider_name}:\n";
    echo "  Calls: " . number_format($provider->calls) . "\n";
    echo "  Tokens: " . number_format($provider->total_tokens) . "\n\n";
}

Budget Alerting ​

Set up real-time alerts for expensive LLM calls:

php
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

// Listen for expensive calls
Event::listen(LlmResponseCompleted::class, function ($event) {
    $span = $event->span;
    $totalTokens = $span->input_tokens + $span->output_tokens;

    // Alert on expensive calls (>10k tokens)
    if ($totalTokens > 10000) {
        Log::warning('Expensive LLM call detected', [
            'model' => $span->request_model,
            'tokens' => $totalTokens,
            'duration_ms' => $span->getDurationInMilliseconds(),
            'span_id' => $span->span_id,
        ]);

        // Send notification
        // Mail::to('admin@example.com')
        //     ->send(new ExpensiveCallAlert($span));
    }
});

Cost Estimation Configuration ​

Mindwave includes pricing for major LLM providers. Update pricing in config/mindwave-tracing.php:

php
'cost_estimation' => [
    'enabled' => env('MINDWAVE_COST_ESTIMATION_ENABLED', true),

    'pricing' => [
        // OpenAI Pricing (per 1000 tokens)
        'openai' => [
            'gpt-4' => [
                'input' => 0.03,
                'output' => 0.06,
            ],
            'gpt-4-turbo' => [
                'input' => 0.01,
                'output' => 0.03,
            ],
            'gpt-3.5-turbo' => [
                'input' => 0.0005,
                'output' => 0.0015,
            ],
        ],

        // Anthropic Claude Pricing (per 1000 tokens)
        'anthropic' => [
            'claude-3-opus' => [
                'input' => 0.015,
                'output' => 0.075,
            ],
            'claude-3-sonnet' => [
                'input' => 0.003,
                'output' => 0.015,
            ],
        ],

        // Mistral AI Pricing (per 1000 tokens)
        'mistral' => [
            'mistral-large-latest' => [
                'input' => 0.004,
                'output' => 0.012,
            ],
        ],
    ],
],

Manual Instrumentation ​

Create custom spans for non-LLM operations or add additional context.

Basic Custom Span ​

php
use Mindwave\Mindwave\Observability\Tracing\TracerManager;

$tracer = app(TracerManager::class);

$span = $tracer->spanBuilder('custom-operation')
    ->setAttribute('user_id', auth()->id())
    ->setAttribute('action', 'export_data')
    ->start();

try {
    // Your custom logic here
    $result = performExpensiveOperation();

    $span->setAttribute('result_count', count($result));
    $span->markAsOk();
} catch (\Exception $e) {
    $span->recordException($e);
    throw $e;
} finally {
    $span->end();
}

Parent-Child Span Relationships ​

Create nested spans to represent hierarchical operations:

php
$tracer = app(TracerManager::class);

// Parent span for batch processing
$parentSpan = $tracer->spanBuilder('batch-process')
    ->setAttribute('batch_size', count($items))
    ->start();

$context = $parentSpan->getContext();

// Process each item with a child span
foreach ($items as $item) {
    $childSpan = $tracer->spanBuilder('process-item')
        ->setParent($context) // Link to parent
        ->setAttribute('item_id', $item->id)
        ->start();

    try {
        processItem($item);
        $childSpan->markAsOk();
    } catch (\Exception $e) {
        $childSpan->recordException($e);
    } finally {
        $childSpan->end();
    }
}

$parentSpan->end();

Wrapping Code in Spans ​

Use the Span::wrap() helper for quick instrumentation:

php
use Mindwave\Mindwave\Observability\Tracing\Span;

// Wrap a database query
$users = Span::wrap('database-query', function () {
    return DB::table('users')
        ->where('active', true)
        ->get();
}, [
    'query_type' => 'select',
    'table' => 'users',
]);

// Wrap an API call
$response = Span::wrap('external-api', function () use ($url) {
    return Http::get($url);
}, [
    'url' => $url,
    'method' => 'GET',
]);

GenAI Custom Spans ​

Create custom spans that follow GenAI semantic conventions:

php
$tracer = app(TracerManager::class);

$span = $tracer->spanBuilder('chat custom-model')
    ->setAttribute('gen_ai.operation.name', 'chat')
    ->setAttribute('gen_ai.provider.name', 'custom-provider')
    ->setAttribute('gen_ai.request.model', 'my-model-v1')
    ->setAttribute('gen_ai.request.temperature', 0.7)
    ->setAttribute('gen_ai.request.max_tokens', 1000)
    ->start();

try {
    // Make custom LLM call
    $response = callCustomLLM();

    // Record usage
    $span->setAttribute('gen_ai.usage.input_tokens', 150);
    $span->setAttribute('gen_ai.usage.output_tokens', 300);
    $span->setAttribute('gen_ai.response.finish_reasons', ['stop']);

    $span->markAsOk();
} catch (\Exception $e) {
    $span->recordException($e);
    throw $e;
} finally {
    $span->end();
}

Adding Events to Spans ​

Record point-in-time events within a span:

php
$span = $tracer->spanBuilder('complex-operation')->start();

$span->addEvent('validation_started');

// ... validation logic ...

$span->addEvent('validation_completed', [
    'validated_items' => 42,
    'errors' => 0,
]);

// ... more processing ...

$span->addEvent('processing_completed', [
    'total_duration_ms' => 1234,
]);

$span->end();

Streaming Traces ​

Mindwave automatically traces streaming LLM responses with real-time events.

Automatic Streaming Instrumentation ​

php
use Mindwave\Mindwave\Facades\Mindwave;

// Stream a response (automatically traced)
$stream = Mindwave::llm()
    ->stream('Write a story about Laravel');

foreach ($stream as $delta) {
    echo $delta; // Output: "Once upon a time..."

    // Behind the scenes:
    // - Span remains open during streaming
    // - LlmTokenStreamed event fired for each delta
    // - Cumulative token count tracked
    // - Span closed when stream completes
}

Listen to Streaming Events ​

php
use Mindwave\Mindwave\Observability\Events\LlmTokenStreamed;
use Illuminate\Support\Facades\Event;

Event::listen(LlmTokenStreamed::class, function ($event) {
    echo "Token: {$event->delta}\n";
    echo "Cumulative: {$event->cumulativeTokens}\n";
    echo "Span: {$event->spanId}\n";
    echo "Trace: {$event->traceId}\n";
});

Streaming Trace Structure ​

mermaid
graph TD
    Trace[Trace: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6]

    Trace --> Span[Span: text_completion gpt-4 client<br/>5.2s total duration]

    Span --> Meta[Metadata]
    Span --> Events[Streaming Events]

    Meta -.-> M1[operation: text_completion<br/>provider: openai<br/>model: gpt-4<br/>output_tokens: 487 cumulative<br/>status: ok]

    Events --> E1[t=100ms<br/>token_streamed token=1]
    Events --> E2[t=110ms<br/>token_streamed token=2]
    Events --> E3[t=120ms<br/>token_streamed token=3]
    Events --> E4[...<br/>484 more events]

    style Trace fill:#e1f5ff
    style Span fill:#fff4e6
    style Meta fill:#f5f5f5
    style Events fill:#e7f9e7
    style E1 fill:#ffe6e6
    style E2 fill:#ffe6e6
    style E3 fill:#ffe6e6
    style E4 fill:#ffe6e6

OTLP Exporters ​

Export traces to production observability platforms using OTLP (OpenTelemetry Protocol).

Jaeger Setup ​

Jaeger is an open-source distributed tracing system.

1. Run Jaeger locally:

bash
docker run -d --name jaeger \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

2. Configure in .env:

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

3. View traces:

Open http://localhost:16686 and search for traces by service name.

Grafana Tempo Setup ​

Tempo is Grafana's high-volume distributed tracing backend.

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

# If using multi-tenancy
OTEL_EXPORTER_OTLP_HEADERS='{"X-Scope-OrgID":"tenant1"}'

Honeycomb Setup ​

Honeycomb is a powerful observability platform with excellent LLM tracing support.

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io:443
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS='{"x-honeycomb-team":"YOUR_API_KEY","x-honeycomb-dataset":"mindwave"}'

Datadog Setup ​

Send traces to Datadog via the OpenTelemetry Collector.

1. Run the OpenTelemetry Collector with Datadog exporter:

yaml
# otel-collector-config.yaml
receivers:
    otlp:
        protocols:
            http:
                endpoint: 0.0.0.0:4318

exporters:
    datadog:
        api:
            key: ${DD_API_KEY}
            site: datadoghq.com

service:
    pipelines:
        traces:
            receivers: [otlp]
            exporters: [datadog]

2. Configure Mindwave:

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

New Relic Setup ​

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS='{"api-key":"YOUR_NEW_RELIC_LICENSE_KEY"}'

Azure Monitor Setup ​

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=https://YOUR_INSTANCE.applicationinsights.azure.com/v2.1/track
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS='{"Authorization":"InstrumentationKey=YOUR_INSTRUMENTATION_KEY"}'

Multi-Exporter Configuration ​

Use both database and OTLP exporters simultaneously:

php
// config/mindwave-tracing.php

'database' => [
    'enabled' => true,
    'connection' => null, // Use default
],

'otlp' => [
    'enabled' => true,
    'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT'),
    'protocol' => env('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/protobuf'),
    'headers' => [],
],

Benefits:

  • Database: Fast local queries with Eloquent
  • OTLP: Distributed tracing across services
  • Both: Local debugging + production monitoring

gRPC Protocol ​

For better performance, use gRPC instead of HTTP:

dotenv
MINDWAVE_TRACE_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc

Note: Requires the PHP grpc extension:

bash
pecl install grpc

Configuration ​

Complete Configuration Reference ​

php
// config/mindwave-tracing.php

return [
    // Enable/disable tracing
    'enabled' => env('MINDWAVE_TRACING_ENABLED', true),

    // Service name (appears in tracing UI)
    'service_name' => env('MINDWAVE_SERVICE_NAME', env('APP_NAME', 'laravel-app')),

    // Database storage
    'database' => [
        'enabled' => env('MINDWAVE_TRACE_DATABASE', true),
        'connection' => env('MINDWAVE_TRACE_DB_CONNECTION', null),
    ],

    // OTLP exporter
    'otlp' => [
        'enabled' => env('MINDWAVE_TRACE_OTLP_ENABLED', false),
        'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318'),
        'protocol' => env('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/protobuf'),
        'headers' => [],
    ],

    // Sampling strategy
    'sampler' => [
        'type' => env('MINDWAVE_TRACE_SAMPLER', 'always_on'),
        'ratio' => (float) env('MINDWAVE_TRACE_SAMPLE_RATIO', 1.0),
    ],

    // Batch processing
    'batch' => [
        'max_queue_size' => 2048,
        'scheduled_delay_ms' => 5000,
        'export_timeout_ms' => 512,
        'max_export_batch_size' => 256,
    ],

    // Privacy
    'capture_messages' => env('MINDWAVE_TRACE_CAPTURE_MESSAGES', false),
    'pii_redact' => [
        'gen_ai.input.messages',
        'gen_ai.output.messages',
        'gen_ai.system_instructions',
        'gen_ai.tool.call.arguments',
        'gen_ai.tool.call.result',
    ],

    // Data retention
    'retention_days' => 30,

    // Cost estimation
    'cost_estimation' => [
        'enabled' => true,
        'pricing' => [
            // See full pricing in config file
        ],
    ],

    // Resource attributes
    'resource_attributes' => [
        'deployment.environment' => env('APP_ENV', 'production'),
        'service.version' => env('APP_VERSION', '1.0.0'),
    ],

    // Instrumentation toggles
    'instrumentation' => [
        'llm' => true,
        'tools' => true,
        'vectorstore' => true,
        'embeddings' => true,
        'memory' => true,
    ],
];

Sampling Strategies ​

Control which traces are recorded:

Always On (Default for development)

dotenv
MINDWAVE_TRACE_SAMPLER=always_on

Always Off (Disable tracing)

dotenv
MINDWAVE_TRACE_SAMPLER=always_off

Trace ID Ratio (Sample percentage)

dotenv
MINDWAVE_TRACE_SAMPLER=traceidratio
MINDWAVE_TRACE_SAMPLE_RATIO=0.1  # Sample 10% of traces

Parent Based (Follow parent span decision)

dotenv
MINDWAVE_TRACE_SAMPLER=parentbased

Environment-Specific Configuration ​

Development:

dotenv
MINDWAVE_TRACING_ENABLED=true
MINDWAVE_TRACE_DATABASE=true
MINDWAVE_TRACE_OTLP_ENABLED=false
MINDWAVE_TRACE_CAPTURE_MESSAGES=true  # OK for local dev
MINDWAVE_TRACE_SAMPLER=always_on
MINDWAVE_TRACE_SAMPLE_RATIO=1.0  # 100%

Production:

dotenv
MINDWAVE_TRACING_ENABLED=true
MINDWAVE_TRACE_DATABASE=false  # Use OTLP only
MINDWAVE_TRACE_OTLP_ENABLED=true
MINDWAVE_TRACE_CAPTURE_MESSAGES=false  # Protect user privacy
MINDWAVE_TRACE_SAMPLER=traceidratio
MINDWAVE_TRACE_SAMPLE_RATIO=0.1  # Sample 10%

Staging:

dotenv
MINDWAVE_TRACING_ENABLED=true
MINDWAVE_TRACE_DATABASE=true  # Keep database for debugging
MINDWAVE_TRACE_OTLP_ENABLED=true  # Also send to observability platform
MINDWAVE_TRACE_CAPTURE_MESSAGES=false
MINDWAVE_TRACE_SAMPLER=always_on
MINDWAVE_TRACE_SAMPLE_RATIO=1.0  # 100%

Privacy & Security ​

PII Redaction ​

By default, Mindwave does not capture prompts and completions to protect sensitive user data.

Safe by default:

dotenv
MINDWAVE_TRACE_CAPTURE_MESSAGES=false  # Default

This redacts:

  • gen_ai.input.messages - User prompts
  • gen_ai.output.messages - AI responses
  • gen_ai.system_instructions - System prompts
  • gen_ai.tool.call.arguments - Function arguments
  • gen_ai.tool.call.result - Function results

Enable message capture only in development or when you have proper data governance:

dotenv
MINDWAVE_TRACE_CAPTURE_MESSAGES=true  # Development only!

Custom PII Redaction ​

Add additional attributes to redact:

php
// config/mindwave-tracing.php

'pii_redact' => [
    'gen_ai.input.messages',
    'gen_ai.output.messages',
    'gen_ai.system_instructions',
    'gen_ai.tool.call.arguments',
    'gen_ai.tool.call.result',
    'gen_ai.embeddings.input',
    'custom.user.email',  // Add custom attributes
    'custom.user.name',
],

Conditional Message Capture ​

Capture messages only for specific users or environments:

php
// In a service provider
use Mindwave\Mindwave\Observability\Tracing\GenAI\GenAiInstrumentor;

app()->singleton(GenAiInstrumentor::class, function ($app) {
    $tracerManager = $app->make(TracerManager::class);

    // Enable message capture only for admins in development
    $captureMessages = app()->environment('local')
        && auth()->check()
        && auth()->user()->isAdmin();

    return new GenAiInstrumentor(
        $tracerManager,
        captureMessages: $captureMessages,
        enabled: config('mindwave-tracing.enabled')
    );
});

Data Retention ​

Automatically prune old traces to comply with data retention policies:

php
// config/mindwave-tracing.php

'retention_days' => 30, // Delete traces older than 30 days

Run the prune command:

bash
php artisan mindwave:prune-traces --older-than=30

Artisan Commands ​

Export Traces ​

Export traces to JSON, CSV, or NDJSON for analysis:

bash
# Export to JSON
php artisan mindwave:export-traces --format=json --output=traces.json

# Export to CSV
php artisan mindwave:export-traces --format=csv --output=traces.csv

# Export to NDJSON (newline-delimited JSON)
php artisan mindwave:export-traces --format=ndjson --output=traces.ndjson

# Export with filters
php artisan mindwave:export-traces \
    --provider=openai \
    --since="2025-01-01" \
    --until="2025-01-31" \
    --format=json \
    --output=january-openai.json

# Filter by cost
php artisan mindwave:export-traces \
    --min-cost=0.10 \
    --format=csv

# Filter by duration
php artisan mindwave:export-traces \
    --slow=5000 \
    --format=json

Prune Old Traces ​

Delete old traces to manage database size:

bash
# Delete traces older than 30 days
php artisan mindwave:prune-traces --older-than=30

# Dry run (see what would be deleted)
php artisan mindwave:prune-traces --older-than=30 --dry-run

# Keep traces with errors
php artisan mindwave:prune-traces --older-than=30 --keep-errors

# Custom batch size
php artisan mindwave:prune-traces --older-than=30 --batch-size=1000

# Skip confirmation
php artisan mindwave:prune-traces --older-than=30 --force

Trace Statistics ​

View comprehensive trace analytics:

bash
# Overall statistics
php artisan mindwave:trace-stats

# Filter by date
php artisan mindwave:trace-stats --since=yesterday

# Filter by provider
php artisan mindwave:trace-stats --provider=openai

# Filter by model
php artisan mindwave:trace-stats --model=gpt-4

Output example:

Mindwave Trace Statistics

Overall Statistics
+---------------------+--------+
| Metric              | Value  |
+---------------------+--------+
| Total Traces        | 1,234  |
| Total Spans         | 2,456  |
| Completed Traces    | 1,230  |
| Avg Spans per Trace | 1.99   |
+---------------------+--------+

Token Usage
+---------------------+-----------+
| Metric              | Value     |
+---------------------+-----------+
| Total Input Tokens  | 150,000   |
| Total Output Tokens | 300,000   |
| Total Tokens        | 450,000   |
| Avg Input Tokens    | 121.65    |
| Avg Output Tokens   | 243.31    |
+---------------------+-----------+

Token Distribution:
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Input: 33.3% | Output: 66.7%

Cost Analysis
+---------------+----------+
| Metric        | Value    |
+---------------+----------+
| Total Cost    | $45.6789 |
| Average Cost  | $0.0370  |
| Min Cost      | $0.0001  |
| Max Cost      | $1.2345  |
+---------------+----------+

Top Models by Usage
+----------------+-------+--------------+------+
| Model          | Uses  | Total Tokens | Chart|
+----------------+-------+--------------+------+
| gpt-4          | 500   | 250,000      | ▓▓▓▓▓|
| gpt-3.5-turbo  | 400   | 180,000      | ▓▓▓▓ |
| claude-3-opus  | 200   | 120,000      | ▓▓   |
+----------------+-------+--------------+------+

Schedule Automatic Pruning ​

Add to app/Console/Kernel.php:

php
protected function schedule(Schedule $schedule)
{
    // Prune traces older than 30 days, daily at 2am
    $schedule->command('mindwave:prune-traces --older-than=30 --force')
        ->dailyAt('02:00');
}

Best Practices ​

Development Environment ​

dotenv
# Development: Full tracing, capture everything
MINDWAVE_TRACING_ENABLED=true
MINDWAVE_TRACE_DATABASE=true
MINDWAVE_TRACE_OTLP_ENABLED=false
MINDWAVE_TRACE_CAPTURE_MESSAGES=true
MINDWAVE_TRACE_SAMPLER=always_on
MINDWAVE_TRACE_SAMPLE_RATIO=1.0

Production Environment ​

dotenv
# Production: Sampled tracing, no PII, OTLP only
MINDWAVE_TRACING_ENABLED=true
MINDWAVE_TRACE_DATABASE=false
MINDWAVE_TRACE_OTLP_ENABLED=true
MINDWAVE_TRACE_CAPTURE_MESSAGES=false
MINDWAVE_TRACE_SAMPLER=traceidratio
MINDWAVE_TRACE_SAMPLE_RATIO=0.1  # 10% sampling

Attribute Naming Conventions ​

Follow OpenTelemetry semantic conventions:

Good:

php
$span->setAttribute('gen_ai.request.model', 'gpt-4');
$span->setAttribute('gen_ai.usage.input_tokens', 150);
$span->setAttribute('user.id', auth()->id());
$span->setAttribute('request.method', 'POST');

Avoid:

php
$span->setAttribute('model', 'gpt-4'); // Too generic
$span->setAttribute('tokens_in', 150); // Non-standard
$span->setAttribute('userId', auth()->id()); // Inconsistent case

Performance Considerations ​

Use Sampling in Production:

dotenv
MINDWAVE_TRACE_SAMPLE_RATIO=0.1  # Only trace 10% of requests

Disable Message Capture:

dotenv
MINDWAVE_TRACE_CAPTURE_MESSAGES=false  # Reduces data volume

Use OTLP Instead of Database:

dotenv
MINDWAVE_TRACE_DATABASE=false  # Database writes can slow down requests
MINDWAVE_TRACE_OTLP_ENABLED=true  # OTLP is optimized for high throughput

Tune Batch Settings:

php
'batch' => [
    'max_queue_size' => 2048,           // Buffer more spans
    'scheduled_delay_ms' => 5000,       // Wait longer before exporting
    'max_export_batch_size' => 512,     // Export in larger batches
],

Error Tracking ​

Always record exceptions in spans:

php
$span = $tracer->spanBuilder('risky-operation')->start();

try {
    // Your code
} catch (\Exception $e) {
    // Record exception with full stack trace
    $span->recordException($e);

    // Optionally add context
    $span->setAttribute('error.type', get_class($e));
    $span->setAttribute('error.handled', true);

    throw $e;
} finally {
    $span->end();
}

Context Propagation ​

When making HTTP requests to other services, propagate trace context:

php
use Illuminate\Support\Facades\Http;
use Mindwave\Mindwave\Observability\Tracing\TracerManager;

$tracer = app(TracerManager::class);
$span = $tracer->spanBuilder('external-api-call')->start();

try {
    // Get traceparent header
    $context = $span->getContext();
    $traceparent = sprintf(
        '00-%s-%s-01',
        $context->getTraceId(),
        $context->getSpanId()
    );

    // Propagate to downstream service
    $response = Http::withHeaders([
        'traceparent' => $traceparent,
    ])->get('https://api.example.com/data');

    $span->markAsOk();
} finally {
    $span->end();
}

Troubleshooting ​

Traces Not Appearing in Database ​

Check configuration:

php
php artisan tinker
>>> config('mindwave-tracing.enabled')
=> true
>>> config('mindwave-tracing.database.enabled')
=> true

Check migrations:

bash
php artisan migrate:status | grep mindwave

Check database connection:

bash
php artisan tinker
>>> \Mindwave\Mindwave\Observability\Models\Trace::count()

Check for errors:

bash
tail -f storage/logs/laravel.log | grep -i trace

OTLP Export Failing ​

Check endpoint:

bash
curl http://localhost:4318/v1/traces
# Should return 405 Method Not Allowed (endpoint exists)

Enable debug logging:

dotenv
LOG_LEVEL=debug

Check logs:

bash
tail -f storage/logs/laravel.log | grep -i otlp

Test with simple HTTP request:

bash
curl -X POST http://localhost:4318/v1/traces \
  -H "Content-Type: application/x-protobuf" \
  --data-binary @/dev/null

High Memory Usage ​

Reduce sampling rate:

dotenv
MINDWAVE_TRACE_SAMPLE_RATIO=0.1  # Sample only 10%

Disable message capture:

dotenv
MINDWAVE_TRACE_CAPTURE_MESSAGES=false

Increase batch export frequency:

php
'batch' => [
    'scheduled_delay_ms' => 2000,  // Export more frequently (every 2s)
    'max_export_batch_size' => 256, // Smaller batches
],

Use OTLP instead of database:

dotenv
MINDWAVE_TRACE_DATABASE=false
MINDWAVE_TRACE_OTLP_ENABLED=true

Slow Performance ​

Disable tracing in critical paths:

php
'instrumentation' => [
    'llm' => true,
    'tools' => false,  // Disable tool tracing
    'vectorstore' => false,  // Disable vectorstore tracing
],

Use asynchronous export:

php
'batch' => [
    'scheduled_delay_ms' => 5000,  // Batch longer
    'max_export_batch_size' => 1000,  // Larger batches
],

Disable database writes:

dotenv
MINDWAVE_TRACE_DATABASE=false

Missing Span Data ​

Check if span ended:

php
$span = $tracer->spanBuilder('test')->start();
// ... do work ...
$span->end();  // Must call end()!

Check sampling:

php
>>> config('mindwave-tracing.sampler.ratio')
=> 1.0  // Should be 1.0 for 100%

Force flush:

php
$tracer = app(TracerManager::class);
$tracer->forceFlush();

Database Connection Issues ​

Specify connection explicitly:

dotenv
MINDWAVE_TRACE_DB_CONNECTION=mysql

Check connection in config:

php
'database' => [
    'enabled' => true,
    'connection' => env('MINDWAVE_TRACE_DB_CONNECTION', 'mysql'),
],

Advanced Examples ​

Correlation with Laravel Logs ​

Add trace IDs to log messages for correlation:

php
use Illuminate\Support\Facades\Log;
use Mindwave\Mindwave\Observability\Tracing\TracerManager;

$tracer = app(TracerManager::class);

// Get current span context
$span = $tracer->startSpan('api-request');
$context = $span->getContext();

Log::info('Processing user request', [
    'trace_id' => $context->getTraceId(),
    'span_id' => $context->getSpanId(),
    'user_id' => auth()->id(),
]);

// Later, search logs by trace_id to find related log entries

Custom Metrics from Traces ​

Calculate SLIs and SLOs from trace data:

php
use Mindwave\Mindwave\Observability\Models\Span;

// Calculate P50, P95, P99 latencies
$latencies = Span::whereDate('created_at', today())
    ->whereNotNull('duration')
    ->pluck('duration')
    ->map(fn($ns) => $ns / 1_000_000) // Convert to ms
    ->sort()
    ->values();

$p50 = $latencies->get((int)($latencies->count() * 0.50));
$p95 = $latencies->get((int)($latencies->count() * 0.95));
$p99 = $latencies->get((int)($latencies->count() * 0.99));

echo "Latency P50: {$p50}ms\n";
echo "Latency P95: {$p95}ms\n";
echo "Latency P99: {$p99}ms\n";

// Calculate error rate
$total = Span::whereDate('created_at', today())->count();
$errors = Span::whereDate('created_at', today())
    ->where('status_code', 'error')
    ->count();
$errorRate = $total > 0 ? ($errors / $total) * 100 : 0;

echo "Error Rate: " . round($errorRate, 2) . "%\n";

Distributed Tracing Across Services ​

Propagate trace context when calling external services:

php
// Service A: Create trace and call Service B
use Illuminate\Support\Facades\Http;
use Mindwave\Mindwave\Observability\Tracing\TracerManager;

$tracer = app(TracerManager::class);
$span = $tracer->spanBuilder('call-service-b')->start();

try {
    $context = $span->getContext();

    // Create W3C traceparent header
    $traceparent = sprintf(
        '00-%s-%s-01',
        $context->getTraceId(),
        $context->getSpanId()
    );

    // Call Service B with trace context
    $response = Http::withHeaders([
        'traceparent' => $traceparent,
    ])->post('https://service-b.com/api/process', [
        'data' => 'example',
    ]);

    $span->setAttribute('response.status', $response->status());
    $span->markAsOk();
} catch (\Exception $e) {
    $span->recordException($e);
    throw $e;
} finally {
    $span->end();
}

// Service B: Extract trace context and continue trace
// OpenTelemetry automatically propagates context from headers!

Real-Time Monitoring Dashboard ​

Build a simple monitoring dashboard:

php
use Mindwave\Mindwave\Observability\Models\Trace;
use Mindwave\Mindwave\Observability\Models\Span;

class TracingDashboardController extends Controller
{
    public function index()
    {
        // Last 24 hours stats
        $since = now()->subDay();

        $stats = [
            'total_traces' => Trace::where('created_at', '>=', $since)->count(),
            'total_cost' => Trace::where('created_at', '>=', $since)->sum('estimated_cost'),
            'total_tokens' => Trace::where('created_at', '>=', $since)
                ->sum(\DB::raw('total_input_tokens + total_output_tokens')),
            'error_rate' => $this->calculateErrorRate($since),
            'avg_latency' => $this->calculateAvgLatency($since),
        ];

        $recentTraces = Trace::with('spans')
            ->orderBy('created_at', 'desc')
            ->limit(10)
            ->get();

        $topModels = Span::where('created_at', '>=', $since)
            ->whereNotNull('request_model')
            ->selectRaw('request_model, COUNT(*) as count')
            ->groupBy('request_model')
            ->orderByDesc('count')
            ->limit(5)
            ->get();

        return view('tracing.dashboard', compact('stats', 'recentTraces', 'topModels'));
    }

    private function calculateErrorRate($since)
    {
        $total = Trace::where('created_at', '>=', $since)->count();
        $errors = Trace::where('created_at', '>=', $since)
            ->where('status', 'error')
            ->count();

        return $total > 0 ? ($errors / $total) * 100 : 0;
    }

    private function calculateAvgLatency($since)
    {
        $avg = Trace::where('created_at', '>=', $since)
            ->whereNotNull('duration')
            ->avg('duration');

        return $avg ? round($avg / 1_000_000, 2) : 0; // Convert ns to ms
    }
}

Resources ​

Official Documentation ​

Backend Documentation ​

Community & Support ​


Next Steps: