Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Instrument an App with OpenTelemetry

By Kokil Thapa | Last reviewed: August 2026

If you cannot trace a request from Nginx through your Laravel application to the database and back, you are debugging blind. To instrument an app with OpenTelemetry effectively in 2026, you must move beyond basic logging and implement structured telemetry that captures latency, errors, and context across service boundaries. This is especially critical for complex systems like legal-tech portals or multi-vendor eCommerce platforms where a single user action triggers multiple backend processes. I recently outlined modern Laravel architecture best practices, and observability is now a non-negotiable pillar of that stack.

How do you set up automatic instrumentation for Laravel in 2026?

Automatic instrumentation is the foundation. It captures incoming HTTP requests, outgoing HTTP calls, database queries, cache operations, and queue jobs without modifying your application code. In 2026, the PHP OpenTelemetry ecosystem has matured significantly; the official Laravel instrumentation package supports Laravel 11 and 12 on PHP 8.2+ reliably.

Install required packages

You need the core SDK, the Laravel-specific instrumentation, and an exporter. For most production setups targeting Jaeger, Grafana Tempo, or Datadog, the OTLP exporter is the standard choice.

composer require open-telemetry/opentelemetry-instrumentation-laravel \
    open-telemetry/exporter-otlp \
    open-telemetry/transport-grpc \
    guzzlehttp/guzzle:^7.0

Note that open-telemetry/transport-grpc requires the gRPC PHP extension. If your hosting environment makes installing extensions difficult (common on shared hosting), use open-telemetry/transport-http instead, which sends OTLP over HTTP/protobuf and works everywhere Guzzle runs.

Configure environment variables

The SDK reads configuration entirely from environment variables following the OpenTelemetry specification. Add these to your .env:

OTEL_SERVICE_NAME=legal-portal-prod
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_SAMPLER=parentbased_tracealways
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_INSTRUMENTATION_LARAVEL_ENABLED=true

In practice, I set OTEL_PHP_AUTOLOAD_ENABLED=true so the SDK hooks into Composer’s autoloader. This eliminates the need to manually bootstrap the tracer in a service provider, reducing the chance of missing early-request spans during framework boot.

Laravel AppPHP 8.4 + OTel SDKAuto-Instrumentation✓ HTTP / Eloquent✓ Redis / QueueOTLP ExporterHTTP/ProtobufBatch ProcessorAsync Non-blockingOTel CollectorJaeger / TempoDatadog / HoneycombStorage + Query
Auto-instrumentation flow: Laravel generates spans automatically and exports them asynchronously via OTLP to your observability backend.

Verify the installation

Create a test route that performs a database query and returns a response. Hit it, then check your observability backend. You should see a root span for the HTTP request with child spans for the Eloquent query and any middleware execution. If spans appear but lack attributes, verify your OTEL_SERVICE_NAME matches what your backend expects.

When should you add manual spans to your application code?

Auto-instrumentation covers infrastructure, not business intent. When debugging why "court marriage applications" take 4 seconds to process on a legal-tech portal I built, auto-instrumentation showed me slow database queries but not that the bottleneck was actually PDF generation happening synchronously inside a controller method. Manual spans bridge this gap.

Creating spans with the global tracer

Use the static Tracer facade provided by the Laravel instrumentation package. This avoids injecting dependencies into every service class:

use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\API\Trace\SpanKind;

class MarriageApplicationService
{
    public function submit(array $data): Application
    {
        $tracer = app(TracerInterface::class);
        $span = $tracer->spanBuilder('marriage.application.submit')
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->setAttribute('application.type', $data['type'])
            ->setAttribute('applicant.district', $data['district'])
            ->startSpan();

        try {
            $application = Application::create($data);
            $this->generatePdf($application);
            $this->notifyLawyer($application);

            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_OK);
            return $application;
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, $e->getMessage());
            throw $e;
        } finally {
            $span->end();
        }
    }
}

Always wrap manual span logic in try/catch/finally. If an exception occurs and you forget to call $span->end(), the span leaks and corrupts trace timing. The finally block guarantees cleanup regardless of success or failure.

Attributes matter more than span names

A span named process.payment is useless if you cannot filter by payment gateway, currency, or transaction amount. Set high-cardinality attributes cautiously (user IDs can explode index size), but always include business-critical dimensions. On eCommerce projects integrating eSewa or Khalti, I consistently tag spans with payment.gateway, order.id, and transaction.status because those are the exact filters needed during incident response.

Need Observability?Is it infrastructure?YESNOAuto-InstrumentationHTTP, DB, Cache, QueueManual SpansBusiness Logic, PDF GenZero code changesAdd attributes + events
Decision framework: infrastructure-level concerns use auto-instrumentation; domain-specific business logic requires manual spans with rich attributes.

What are the production safety considerations for PHP telemetry?

Instrumentation adds overhead. In my experience working on production Laravel applications serving thousands of daily users, unconfigured OpenTelemetry can increase p99 latency by 15–30% and consume significant memory during traffic spikes. These safeguards are mandatory, not optional.

Sampling strategy

Never sample at 100% in production unless your volume is very low. Use parent-based sampling to respect upstream decisions, combined with a rate limiter:

OTEL_TRACES_SAMPLER=parentbased_tracealways
# Or for head-based sampling at 10%:
# OTEL_TRACES_SAMPLER=traceidratio
# OTEL_TRACES_SAMPLER_ARG=0.1

For high-traffic eCommerce sites during Dashain/Tihar sales periods, I typically drop to 5% sampling. You still capture enough data to identify patterns while keeping collector costs manageable. Remember: sampling happens at the SDK level before export, so unsampled spans never leave your server.

Batch processing and timeouts

The default batch processor buffers spans and flushes periodically. Tune these for your workload:

OTEL_BSP_SCHEDULE_DELAY=5000
OTEL_BSP_EXPORT_TIMEOUT=30000
OTEL_BSP_MAX_QUEUE_SIZE=2048
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512

If your exporter endpoint is slow or unreachable, the queue fills up. Once MAX_QUEUE_SIZE is reached, new spans are dropped silently. Monitor otel.exporter.otlp.failed_spans metrics to detect this condition. On one project, we discovered our collector was rejecting batches due to payload size limits; without monitoring export failures, we lost hours of trace data during a critical outage.

Context propagation across services

If your Laravel app calls external APIs or microservices, ensure W3C Trace Context headers propagate correctly. The auto-instrumentation handles outgoing HTTP requests automatically, but custom Guzzle clients or cURL calls may bypass it. Always use the instrumented HTTP client or manually inject context:

use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;

$headers = [];
TraceContextPropagator::getInstance()->inject($headers);

$response = Http::withHeaders($headers)->get('https://api.nepal-gift-card.com/v1/cards');

Without propagation, downstream services create orphaned traces disconnected from the originating request. This is particularly painful when debugging cross-service workflows like payment verification callbacks from ConnectIPS or IME Pay.

ConfigurationDevelopmentProduction (Low Traffic)Production (High Traffic)
Sampleralways_onparentbased_tracealwaystraceidratio (0.05–0.1)
Export Protocolhttp/protobufhttp/protobufgrpc (lower overhead)
Batch Delay1000ms5000ms5000–10000ms
Max Queue Size51220484096+
Attribute LimitsDefaultDefaultCap at 128 attrs/span
Exception RecordingAll exceptionsAll exceptionsOnly 5xx / critical

How do you validate OpenTelemetry instrumentation before deploying?

Never deploy instrumentation changes directly to production without validation. Broken telemetry is worse than no telemetry because it creates false confidence.

Local validation with console exporter

During development, switch to the console exporter to see spans in your terminal:

OTEL_EXPORTER_OTLP_ENDPOINT=""
OTEL_TRACES_EXPORTER=console
OTEL_LOG_LEVEL=debug

This outputs JSON-formatted spans to stdout. Verify span names follow conventions (GET /api/applications/{id} not handle), attributes contain expected values, and parent-child relationships form coherent trees. If your local Docker Compose includes Jaeger, point the OTLP endpoint there for visual inspection.

Staging environment smoke tests

Deploy to staging first and run automated tests that exercise key user journeys. After each test run, query your observability backend programmatically to assert expected spans exist. For example, after submitting a marriage application, verify a span named marriage.application.submit exists with attribute application.type=court. Catching missing instrumentation in staging prevents silent failures in production.

Local DevConsole ExporterJSON to stdout✓ Immediate feedback✓ No network needed✗ Not representativeStagingReal OTLP BackendAutomated Assertions✓ Full pipeline test✓ Catch config errors✗ Limited load profileProductionSampled ExportMonitoring Alerts✓ Real traffic patterns✓ Cost-controlled✗ Hard to reproduce
Validation progression: start locally with console output, verify in staging with real backend assertions, then deploy to production with sampling and alerting.

Performance benchmarking

Before enabling instrumentation in production, benchmark your application with and without the SDK loaded. Use Apache Bench or k6 against a representative endpoint:

k6 run --iterations=1000 --concurrency=50 bench.js

Compare p50, p95, and p99 latencies. If overhead exceeds 10%, investigate whether excessive attributes, synchronous exporters, or misconfigured batching is the cause. On a recent Laravel API project, we reduced instrumentation overhead from 18% to 4% simply by switching from gRPC to HTTP/protobuf (our server lacked the gRPC extension and was falling back to a slow polyfill).

Common pitfalls when implementing OpenTelemetry in PHP

After helping teams adopt observability across multiple Nepali and international projects, certain mistakes recur consistently.

  • Over-instrumenting trivial operations: Wrapping every helper function in a span creates noise, not signal. Reserve manual spans for operations taking >50ms or representing meaningful business steps.
  • Ignoring baggage propagation limits: Baggage (key-value pairs propagated across services) has size limits. Stuffing entire user objects into baggage causes silent truncation. Use baggage only for routing hints and correlation IDs.
  • Forgetting CLI and queue workers: Auto-instrumentation activates differently in long-running processes. Queue workers may hold stale tracer state across jobs. Restart workers after SDK updates and verify job spans link to their triggering HTTP request.
  • Mixing incompatible package versions: The PHP OpenTelemetry ecosystem moves fast. Pin exact versions in composer.json and test upgrades in isolation. A mismatched opentelemetry-api and opentelemetry-sdk version can silently disable instrumentation.
  • Neglecting log correlation: Traces without correlated logs force context-switching between tools. Configure your logger to inject trace_id and span_id into every log entry. Most Laravel logging channels support this via the OpenTelemetry log bridge.

For teams building REST APIs in Laravel, pay special attention to error handling middleware. Unhandled exceptions that bypass your error handler won’t be recorded as span events. Ensure your exception handler explicitly records exceptions on the active span before re-throwing or returning error responses.

Start instrumenting with intention, not defaults

To instrument an app with OpenTelemetry successfully, treat observability as a first-class engineering concern rather than an afterthought. Begin with auto-instrumentation to establish baseline visibility, add manual spans surgically for business-critical paths, and enforce production safeguards from day one. The goal isn’t maximum data collection—it’s actionable insight with minimal overhead. If you’re planning a new build or modernizing an existing system, review what’s changed in Laravel 12 to understand native observability improvements that complement OpenTelemetry. Need help designing an observability strategy tailored to your application’s scale and budget? Get in touch to discuss your specific requirements.

Frequently Asked Questions

PHP 8.2 or higher is required for current OpenTelemetry PHP SDKs and auto-instrumentation extensions in 2026.

The SDK and collector are free open-source software; costs involve only backend storage and engineering time, typically Rs 15,000–40,000 (~USD 110–300) for initial setup.

Use auto-instrumentation for framework coverage like Laravel routing and database queries; reserve manual instrumentation for custom business logic and specific transaction boundaries.

Install the opentelemetry extension via PECL compiled against your active PHP 8.3 or 8.4 binary, then enable it in /etc/php/8.x/fpm/conf.d/. Configure the OTEL_PHP_AUTOLOAD_ENABLED environment variable in your .env file and restart PHP-FPM to load the extension. Verify installation by checking php -m output and confirming traces appear in your configured exporter endpoint after generating test traffic.

Overhead typically ranges from one to three percent when using batched exporters and sampling. Synchronous exporting causes significant latency spikes, so always configure async batch processing via the OTLP gRPC or HTTP exporter. In my experience deploying instrumented Laravel apps on shared EC2 infrastructure, setting OTEL_TRACES_SAMPLER to parentbased_tracealways with a ratio of 0.1 prevents trace volume from overwhelming application threads during peak traffic periods.

Yes, OpenTelemetry PHP auto-instrumentation hooks into WordPress and WooCommerce action filters automatically when the extension is loaded. For deeper visibility into checkout flows or payment gateway callbacks like eSewa or Khalti, add manual spans in custom mu-plugins or theme functions.php. This approach preserves upgrade safety while capturing business-critical transaction data that generic auto-instrumentation misses entirely in complex eCommerce workflows.

Self-hosted Jaeger or Grafana Tempo on local VPS keeps data within Nepal and avoids international bandwidth costs for high-volume telemetry. Cloud options like Datadog or New Relic charge per GB ingested and can exceed Rs 50,000 monthly for busy sites. For most Nepali SMB projects I work on, a single-server Tempo instance with local SSD storage provides adequate retention at roughly Rs 5,000–10,000 monthly hosting cost.

Propagate W3C Trace Context headers using the built-in Guzzle or HTTP client middleware provided by the OpenTelemetry SDK. When integrating third-party services like ConnectIPS or SMS gateways, ensure outbound requests include the traceparent header. On the receiving service side, extract and continue the trace context. Without proper propagation, each service generates isolated traces making end-to-end debugging impossible across microservice or multi-vendor payment architectures.

Eloquent ORM auto-instrumentation requires the opentelemetry-instrumentation-eloquent package alongside the core extension. Verify PDO instrumentation is also enabled since some query builders bypass Eloquent. Check that sensitive query parameters are not being redacted by default security configurations. In production Laravel applications I have debugged, missing DB spans usually stem from composer package version mismatches between the SDK and instrumentation libraries after framework upgrades.

Always encrypt OTLP exports using TLS 1.2 or higher when transmitting outside localhost. Configure mTLS certificates if your collector requires mutual authentication. Never expose the OTLP receiver port publicly without authentication; use reverse proxy rules or firewall restrictions. For legal-tech portals handling sensitive client documents, I route all telemetry through internal VPC networks before forwarding aggregated metrics to external observability platforms to maintain compliance boundaries.

Enable debug logging via OTEL_LOG_LEVEL=debug and check PHP error logs for exporter connection timeouts or authentication rejections. Validate network connectivity from the application server to the collector endpoint using curl with matching headers. Confirm DNS resolution works inside containers or chroot environments. Common issues I encounter involve stale cached configurations after deployment or PHP-FPM worker processes holding outdated environment variables despite config reloads.

Head-based sampling at ten percent captures sufficient signal for most production web applications while controlling storage costs. Parent-based sampling ensures complete traces for sampled requests including all child spans. Avoid tail-based sampling unless you need error-only retention, as it requires buffering entire traces in memory. For high-traffic directories or marketplaces, adaptive sampling based on response status codes provides better incident visibility than uniform random selection.

No, traces complement but do not replace structured logs. Traces show request flow and latency across services; logs capture detailed business context, validation errors, and audit events. Correlate both by injecting trace_id and span_id into Monolog or Laravel log channels. This dual approach lets you jump from a slow trace directly to relevant log entries. Removing logs entirely loses searchable text data essential for debugging non-performance issues.

Run the instrumented application in staging with console exporter enabled to inspect trace structure locally. Generate synthetic traffic covering critical paths like authentication, checkout, and API integrations. Compare span counts and durations against baseline expectations. Deploy to a canary subset first using feature flags or weighted load balancing. I never enable full instrumentation on production servers without this validation step, as misconfigured exporters have caused cascading failures in live environments.

Loading the extension without configuring exporters silently discards all telemetry. Running incompatible SDK and extension versions after PHP upgrades causes segmentation faults. Forgetting to restart PHP-FPM after config changes leaves workers using stale settings. Setting synchronous export mode in production blocks request threads. Always pin compatible package versions in composer.json, test extension loading in CI pipelines, and verify export functionality in staging before merging instrumentation changes to main branches.

Share this article

Quick Contact Options
Choose how you want to connect me: