├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Readme.md ├── composer.json └── src ├── Helpers └── Helper.php ├── Logger ├── OtelLogger.php └── OtelLoggerFactory.php ├── Middleware ├── OpenTelemetryMetricsMiddleware.php └── OpenTelemetryTraceMiddleware.php ├── Providers └── OpenTelemetryServiceProvider.php ├── Services ├── MetricsService.php └── TraceService.php └── config └── opentelemetry.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # .github/FUNDING.yml 2 | custom: 3 | - https://www.buymeacoffee.com/i_sazzad 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Imranul Sazzad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry Laravel Package 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/laratel/opentelemetry.svg?style=flat&color=blue)](https://packagist.org/packages/laratel/opentelemetry) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/laratel/opentelemetry.svg?style=flat&color=green)](https://packagist.org/packages/laratel/opentelemetry) 5 | 6 | **OpenTelemetry Laravel** is a Laravel package that integrates OpenTelemetry for automatic HTTP request tracing, query tracing, metrics collection, and enhanced logging with contextual trace information. 7 | 8 | --- 9 | 10 | ## Features 11 | 12 | - Automatic HTTP request tracing. 13 | - Automatic database query tracing with detailed SQL metrics. 14 | - Metrics collection for HTTP requests, database queries, and system performance. 15 | - Enhanced logging with contextual trace information. 16 | - Middleware support for seamless integration. 17 | - Customizable configuration. 18 | 19 | --- 20 | 21 | ## Requirements 22 | 23 | - PHP >= 8.0 24 | - Laravel >= 9.x 25 | - Dependencies: 26 | - `open-telemetry/exporter-otlp` ^1.1 27 | - `open-telemetry/sdk` ^1.1 28 | - `open-telemetry/transport-grpc` ^1.1 29 | 30 | --- 31 | 32 | ## Installation 33 | 34 | ### 1. Install via Composer 35 | 36 | ```bash 37 | composer require laratel/opentelemetry 38 | ``` 39 | 40 | ⚠️ If you encounter the following error: 41 | 42 | ``` 43 | open-telemetry/transport-grpc 1.1.3 requires ext-grpc * -> it is missing from your system. 44 | ``` 45 | 46 | It means the **gRPC PHP extension** is not installed or enabled. You can fix this by: 47 | 48 | - Enabling the `grpc` extension in your `php.ini` file: 49 | 50 | ``` 51 | extension=grpc 52 | ``` 53 | 54 | For example: `C:\xampp\php\php.ini` on Windows. 55 | 56 | - Restart your web server (Apache, Nginx) or PHP-FPM service after making changes. 57 | 58 | - Alternatively, to bypass this requirement during development, use: 59 | 60 | ```bash 61 | composer require laratel/opentelemetry --ignore-platform-req=ext-grpc 62 | ``` 63 | 64 | --- 65 | 66 | ### 2. Register the Service Provider 67 | 68 | Add the service provider to the `providers` array in `config/app.php` (this step is optional if your package uses auto-discovery): 69 | 70 | ```php 71 | 'providers' => [ 72 | // Other Service Providers... 73 | Laratel\Opentelemetry\Providers\OpenTelemetryServiceProvider::class, 74 | ], 75 | ``` 76 | 77 | --- 78 | 79 | ### 3. Publish the Configuration File 80 | 81 | Publish the configuration file to your application: 82 | 83 | ```bash 84 | php artisan vendor:publish --provider="Laratel\Opentelemetry\Providers\OpenTelemetryServiceProvider" 85 | ``` 86 | 87 | This will create a configuration file at `config/opentelemetry.php`. Update the settings as needed, such as the OTLP endpoint, excluded routes, and logging configuration. 88 | 89 | --- 90 | 91 | ## Configuration 92 | 93 | ### OpenTelemetry Configuration 94 | 95 | The `config/opentelemetry.php` file allows you to configure: 96 | 97 | - **OTLP Endpoint**: 98 | Specify the OTLP collector endpoint for sending telemetry data. 99 | 100 | - **Excluded Routes**: 101 | Define routes to exclude from tracing or metrics collection. 102 | 103 | - **Excluded Queries**: 104 | Specify database queries that should not be traced. 105 | 106 | --- 107 | 108 | ### Log Channel Configuration 109 | 110 | Add the following configuration to `config/logging.php` for enhanced OpenTelemetry logging: 111 | 112 | ```php 113 | 'otel' => [ 114 | 'driver' => 'custom', 115 | 'via' => Laratel\Opentelemetry\Logger\OtelLoggerFactory::class, 116 | 'level' => 'debug', 117 | ], 118 | ``` 119 | 120 | --- 121 | 122 | ## Usage 123 | 124 | ## Environment Variables 125 | 126 | To configure OpenTelemetry via environment variables, include the following in your `.env` file: 127 | 128 | ```env 129 | OTEL_SERVICE_NAME=your_service_name 130 | OTEL_PHP_AUTOLOAD_ENABLED=true 131 | OTEL_TRACES_EXPORTER=otlp 132 | OTEL_METRICS_EXPORTER=otlp 133 | OTEL_LOGS_EXPORTER=otlp 134 | OTEL_PROPAGATORS=baggage,tracecontext 135 | OTEL_TRACES_SAMPLER=always_on 136 | OTEL_EXPORTER_OTLP_PROTOCOL=grpc 137 | OTEL_EXPORTER_OTLP_ENDPOINT=http://your_otel_collector_endpoint:port 138 | OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.namespace=service_namespace,service.version=1.0,service.instance.id=instance_id 139 | ``` 140 | 141 | --- 142 | 143 | ### Middleware 144 | 145 | The package provides two middleware for automatic tracing and metrics collection: 146 | 147 | 1. **`opentelemetry.metrics`**: Collects HTTP and system metrics. 148 | 2. **`opentelemetry.trace`**: Captures tracing information for HTTP requests and database queries. 149 | 150 | #### Register Middleware in Kernel 151 | 152 | To apply middleware globally, add them to the `$middleware` array in `app/Http/Kernel.php`: 153 | 154 | ```php 155 | protected $middleware = [ 156 | \Laratel\Opentelemetry\Middleware\OpenTelemetryMetricsMiddleware::class, 157 | \Laratel\Opentelemetry\Middleware\OpenTelemetryTraceMiddleware::class, 158 | ]; 159 | ``` 160 | 161 | #### Register Middleware Aliases 162 | 163 | Alternatively, register middleware aliases in the `Kernel` class: 164 | 165 | ```php 166 | protected $routeMiddleware = [ 167 | 'opentelemetry.metrics' => \Laratel\Opentelemetry\Middleware\OpenTelemetryMetricsMiddleware::class, 168 | 'opentelemetry.trace' => \Laratel\Opentelemetry\Middleware\OpenTelemetryTraceMiddleware::class, 169 | ]; 170 | ``` 171 | 172 | #### Use Middleware in Routes 173 | 174 | Once aliases are registered, use them in your routes: 175 | 176 | ```php 177 | Route::middleware(['opentelemetry.metrics', 'opentelemetry.trace'])->group(function () { 178 | Route::get('api/example', function () { 179 | return response()->json(['message' => 'Tracing and metrics enabled']); 180 | }); 181 | }); 182 | ``` 183 | 184 | --- 185 | 186 | ### Logging 187 | 188 | Use the `otel` log channel for enhanced logging with trace context: 189 | 190 | ```php 191 | use Illuminate\Support\Facades\Log; 192 | 193 | Log::channel('otel')->info('Test log for OpenTelemetry collector', ['user' => 'example']); 194 | ``` 195 | 196 | Logs will include trace information and be sent to the configured OpenTelemetry collector. 197 | 198 | --- 199 | 200 | ### Automatic Query Tracing 201 | 202 | The package automatically traces database queries. Traces include: 203 | 204 | - SQL statements 205 | - Bindings 206 | - Execution times 207 | 208 | You can customize which queries to exclude using the `config/opentelemetry.php` file. 209 | 210 | --- 211 | 212 | ### Custom Instrumentation 213 | 214 | #### Custom Traces 215 | 216 | Use the `TraceService` to create custom traces: 217 | 218 | ```php 219 | use Laratel\Opentelemetry\Services\TraceService; 220 | 221 | $traceService = new TraceService(); 222 | $tracer = $traceService->getCustomTracer(); 223 | 224 | $span = $tracer->spanBuilder('custom-operation')->startSpan(); 225 | $span->setAttribute('custom.attribute', 'value'); 226 | // Perform some operation 227 | $span->end(); 228 | ``` 229 | 230 | --- 231 | 232 | ## Repository for Related Tools and Configurations 233 | 234 | Find a complete repository containing Docker Compose file, configuration files for OpenTelemetry Collector, Prometheus, Tempo, Loki, Promtail and Grafana [here](https://github.com/i-sazzad/otel). 235 | 236 | --- 237 | 238 | ## Contributing 239 | 240 | Contributions are welcome! Please fork the repository, create a feature branch, and submit a pull request. 241 | 242 | --- 243 | 244 | ## License 245 | 246 | This package is open-source software licensed under the [MIT license](LICENSE). 247 | 248 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laratel/opentelemetry", 3 | "description": "A Laravel package for OpenTelemetry tracing, metrics, and logging integration.", 4 | "type": "library", 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=8.0", 8 | "open-telemetry/exporter-otlp": "^1.1", 9 | "open-telemetry/sdk": "^1.1", 10 | "open-telemetry/transport-grpc": "^1.1", 11 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9.5 || ^10.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Laratel\\Opentelemetry\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Laratel\\Opentelemetry\\Tests\\": "tests/" 24 | } 25 | }, 26 | "extra": { 27 | "laravel": { 28 | "providers": [ 29 | "Laratel\\Opentelemetry\\Providers\\OpenTelemetryServiceProvider" 30 | ] 31 | } 32 | }, 33 | "scripts": { 34 | "test": "phpunit" 35 | }, 36 | "repositories": [ 37 | { 38 | "type": "vcs", 39 | "url": "https://github.com/i-sazzad/laravel-opentelemetry" 40 | } 41 | ], 42 | "minimum-stability": "stable", 43 | "prefer-stable": true, 44 | "keywords": ["laravel", "laratel", "opentelemetry", "tracing", "metrics", "logging", "real-time monitoring", "observability", "performance monitoring", "telemetry", "PHP", "application performance", "open-source", "middleware", 45 | "distributed tracing", "monitoring tools", "error tracking", "request tracing", "API monitoring", "metrics exporter", "performance profiling", "cloud observability", "scalable monitoring", "debugging", "error logs", 46 | "visualizing performance", "telemetry SDK", "opentelemetry exporter", "event-driven architecture", "OTLP", "grpc", "PHP telemetry integration", "open-source observability", "backend monitoring", 47 | "microservices monitoring", "service monitoring", "laravel-opentelemetry"] 48 | } 49 | -------------------------------------------------------------------------------- /src/Helpers/Helper.php: -------------------------------------------------------------------------------- 1 | forceFlush(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Logger/OtelLogger.php: -------------------------------------------------------------------------------- 1 | loggerProvider = $loggerProvider; 18 | } 19 | 20 | public function log($level, $message, array $context = []): void 21 | { 22 | $logRecord = (new LogRecord($message)) 23 | ->setTimestamp((int) (microtime(true) * 1_000_000_000)) // Timestamp of log creation 24 | ->setObservedTimestamp((int) (microtime(true) * 1_000_000_000)) // Timestamp of log observation 25 | ->setSeverityNumber($this->mapSeverityNumber($level)) // Map severity to OpenTelemetry number 26 | ->setSeverityText(strtoupper($level)) // Use log level as severity text 27 | ->setAttributes($context) // Attach the context as log attributes 28 | ->setContext(Context::getCurrent()); // Attach the current context 29 | 30 | // Emit the log record 31 | $this->loggerProvider->getLogger('otel_logger')->emit($logRecord); 32 | } 33 | 34 | public function emergency($message, array $context = []): void 35 | { 36 | $this->log(LogLevel::EMERGENCY, $message, $context); 37 | } 38 | 39 | public function alert($message, array $context = []): void 40 | { 41 | $this->log(LogLevel::ALERT, $message, $context); 42 | } 43 | 44 | public function critical($message, array $context = []): void 45 | { 46 | $this->log(LogLevel::CRITICAL, $message, $context); 47 | } 48 | 49 | public function error($message, array $context = []): void 50 | { 51 | $this->log(LogLevel::ERROR, $message, $context); 52 | } 53 | 54 | public function warning($message, array $context = []): void 55 | { 56 | $this->log(LogLevel::WARNING, $message, $context); 57 | } 58 | 59 | public function notice($message, array $context = []): void 60 | { 61 | $this->log(LogLevel::NOTICE, $message, $context); 62 | } 63 | 64 | public function info($message, array $context = []): void 65 | { 66 | $this->log(LogLevel::INFO, $message, $context); 67 | } 68 | 69 | public function debug($message, array $context = []): void 70 | { 71 | $this->log(LogLevel::DEBUG, $message, $context); 72 | } 73 | 74 | private function mapSeverityNumber(string $level): int 75 | { 76 | return match ($level) { 77 | LogLevel::EMERGENCY, LogLevel::ALERT => 1, // FATAL 78 | LogLevel::CRITICAL => 3, // ERROR 79 | LogLevel::ERROR => 4, // ERROR 80 | LogLevel::WARNING => 5, // WARN 81 | LogLevel::NOTICE => 6, // INFO 82 | LogLevel::INFO => 7, // INFO 83 | LogLevel::DEBUG => 8, // DEBUG 84 | default => 8, // Default to DEBUG 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Logger/OtelLoggerFactory.php: -------------------------------------------------------------------------------- 1 | isOpenTelemetryServerReachable()) { 28 | return false; 29 | } 30 | 31 | $logExporter = match ($protocol) { 32 | 'grpc' => new LogsExporter( 33 | (new GrpcTransportFactory())->create( 34 | $endpoint . '/opentelemetry.proto.collector.logs.v1.LogsService/Export', 35 | 'application/x-protobuf' 36 | ) 37 | ), 38 | 'http' => new LogsExporter( 39 | (new OtlpHttpTransportFactory())->create( 40 | $endpoint . '/v1/logs', 41 | 'application/json' 42 | ) 43 | ), 44 | default => throw new \InvalidArgumentException("Unsupported protocol: $protocol"), 45 | }; 46 | 47 | // Create a log processor with configured batch settings 48 | $logProcessor = new BatchLogRecordProcessor($logExporter, new SystemClock(), 2048, 1000000000, 512); 49 | 50 | // Prepare the instrumentation and resource info 51 | $attributesFactory = new AttributesFactory(); 52 | $instrumentationScopeFactory = new InstrumentationScopeFactory($attributesFactory); 53 | $resource = ResourceInfoFactory::defaultResource(); 54 | 55 | // Create the Logger Provider 56 | $loggerProvider = new LoggerProvider($logProcessor, $instrumentationScopeFactory, $resource); 57 | 58 | // Register a shutdown function to ensure the logs are exported on shutdown 59 | register_shutdown_function(function () use ($logProcessor) { 60 | try { 61 | $logProcessor->shutdown(); 62 | } catch (\Throwable $e) { 63 | throw new \Exception('Error during OpenTelemetry logger shutdown: ' . $e->getMessage()); 64 | } 65 | }); 66 | 67 | return new OtelLogger($loggerProvider); 68 | } 69 | 70 | private function isOpenTelemetryServerReachable(): bool 71 | { 72 | $endpoint = config('opentelemetry.endpoint'); 73 | 74 | if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { 75 | Log::error('Invalid OTEL_EXPORTER_OTLP_ENDPOINT URL provided.'); 76 | return false; 77 | } 78 | 79 | $host = parse_url($endpoint, PHP_URL_HOST); 80 | $port = parse_url($endpoint, PHP_URL_PORT) ?? 4318; 81 | 82 | $connection = @fsockopen($host, $port, $errno, $err_str, 2); 83 | 84 | if ($connection) { 85 | fclose($connection); 86 | return true; 87 | } else { 88 | Log::error('Unreachable OTEL_EXPORTER_OTLP_ENDPOINT URL provided.'); 89 | return false; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Middleware/OpenTelemetryMetricsMiddleware.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 21 | } 22 | 23 | /** 24 | * Handles an incoming request and records metrics. 25 | * 26 | * @throws Throwable 27 | */ 28 | public function handle(Request $request, Closure $next): Response 29 | { 30 | $metrics = new MetricsService(); 31 | 32 | // Skip recording if metrics are not available 33 | if (!$metrics->metrics) { 34 | return $next($request); 35 | } 36 | 37 | // Skip recording metrics if the route is excluded 38 | if ($this->helper->shouldExclude($request->path())) { 39 | return $next($request); 40 | } 41 | 42 | // Skip if metrics are already recorded for this request 43 | if ($request->attributes->get('metrics_recorded', false)) { 44 | return $next($request); 45 | } 46 | 47 | // Mark metrics as recorded for this request 48 | $request->attributes->set('metrics_recorded', true); 49 | 50 | // Start the timer to record the duration of the request processing 51 | $this->startTime = microtime(true); 52 | 53 | try { 54 | $response = $next($request); 55 | 56 | // Start recording database and cache metrics 57 | DB::listen(function ($query) use ($metrics) { 58 | $metrics->recordDbMetrics($query); // Record database query metrics 59 | }); 60 | 61 | $metrics->wrapCacheOperations(); // Record cache operations 62 | 63 | // Record HTTP and system metrics 64 | $metrics->recordMetrics($request, $response, $this->startTime); 65 | $metrics->recordSystemMetrics(); // Record system-level metrics 66 | $metrics->recordNetworkMetrics(); // Record network-level metrics 67 | } catch (Throwable $e) { 68 | $metrics->recordErrorMetrics($e); 69 | 70 | throw $e; 71 | } finally { 72 | // Flush metrics data to the collector, only if metrics were successfully captured 73 | $this->helper->flushMetrics(); 74 | } 75 | 76 | return $response; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Middleware/OpenTelemetryTraceMiddleware.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 18 | } 19 | 20 | /** 21 | * @throws Throwable 22 | */ 23 | public function handle(Request $request, Closure $next) 24 | { 25 | $trace = new TraceService(); 26 | 27 | $tracer = $trace->getTracer(); 28 | 29 | // Skip tracing if tracer is not available 30 | if (!$tracer) { 31 | return $next($request); 32 | } 33 | 34 | // Skip tracing for excluded paths 35 | if ($this->helper->shouldExclude($request->path())) { 36 | return $next($request); 37 | } 38 | 39 | $trace->dbQueryTrace(); // Set up DB query trace if necessary 40 | 41 | // Start a new span for the request 42 | $span = $tracer->spanBuilder($request->method() . ' ' . $request->path())->startSpan(); 43 | $scope = $span->activate(); 44 | $startTime = microtime(true); 45 | 46 | try { 47 | $response = $next($request); 48 | 49 | // Set span attributes and add custom events 50 | $trace->setSpanAttributes($span, $request, $response); 51 | $trace->addRouteEvents($span, $request, $response, $startTime); 52 | } catch (Throwable $e) { 53 | $trace->handleException($span, $e); 54 | $trace->setSpanAttributes($span, $request, null); 55 | $trace->addRouteEvents($span, $request, null, $startTime); 56 | 57 | throw $e; 58 | } finally { 59 | // Always ensure that the span is ended and detached 60 | $scope?->detach(); 61 | $span?->end(); 62 | } 63 | 64 | return $response; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Providers/OpenTelemetryServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/opentelemetry.php', 'opentelemetry'); 32 | 33 | if($this->isOpenTelemetryServerReachable()){ 34 | $this->registerTracer(); 35 | $this->registerMetrics(); 36 | } 37 | } 38 | 39 | private function registerTracer(): void 40 | { 41 | $this->app->singleton('tracer', function () { 42 | $endpoint = config('opentelemetry.endpoint'); 43 | $protocol = config('opentelemetry.protocol'); 44 | 45 | try { 46 | $spanExporter = match ($protocol) { 47 | 'grpc' => new SpanExporter( 48 | (new GrpcTransportFactory())->create( 49 | $endpoint . '/opentelemetry.proto.collector.trace.v1.TraceService/Export', 50 | 'application/x-protobuf' 51 | ) 52 | ), 53 | 'http' => new SpanExporter( 54 | (new OtlpHttpTransportFactory())->create( 55 | $endpoint . '/v1/traces', 56 | 'application/json' 57 | ) 58 | ), 59 | default => throw new \InvalidArgumentException("Unsupported protocol: $protocol"), 60 | }; 61 | 62 | return (new TracerProvider( 63 | new SimpleSpanProcessor($spanExporter), 64 | new AlwaysOnSampler(), 65 | ResourceInfoFactory::defaultResource() 66 | ))->getTracer('otel_tracer'); 67 | } catch (\Exception $e) { 68 | Log::error('OpenTelemetry Tracer connection error: ' . $e->getMessage(), [ 69 | 'exception' => $e->getMessage(), 70 | 'stack' => $e->getTraceAsString() 71 | ]); 72 | return null; // Return null if connection fails 73 | } 74 | }); 75 | } 76 | 77 | private function registerMetrics(): void 78 | { 79 | $this->app->singleton('meterProvider', function () { 80 | $endpoint = config('opentelemetry.endpoint'); 81 | $protocol = config('opentelemetry.protocol'); 82 | 83 | try { 84 | $metricExporter = match ($protocol) { 85 | 'grpc' => new MetricExporter( 86 | (new GrpcTransportFactory())->create( 87 | $endpoint . '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', 88 | 'application/x-protobuf' 89 | ) 90 | ), 91 | 'http' => new MetricExporter( 92 | (new OtlpHttpTransportFactory())->create( 93 | $endpoint . '/v1/metrics', 94 | 'application/json' 95 | ) 96 | ), 97 | default => throw new \InvalidArgumentException("Unsupported protocol: $protocol"), 98 | }; 99 | 100 | $attributesFactory = new AttributesFactory(); 101 | 102 | return new MeterProvider( 103 | contextStorage: new ContextStorage(), 104 | resource: ResourceInfoFactory::defaultResource(), 105 | clock: new SystemClock(), 106 | attributesFactory: $attributesFactory, 107 | instrumentationScopeFactory: new InstrumentationScopeFactory($attributesFactory), 108 | metricReaders: [new ExportingReader($metricExporter)], 109 | viewRegistry: new CriteriaViewRegistry(), 110 | exemplarFilter: null, 111 | stalenessHandlerFactory: new ImmediateStalenessHandlerFactory(), 112 | metricFactory: new StreamFactory(), 113 | configurator: null 114 | ); 115 | } catch (\Exception $e) { 116 | Log::error('OpenTelemetry Metrics connection error: ' . $e->getMessage(), [ 117 | 'exception' => $e->getMessage(), 118 | 'stack' => $e->getTraceAsString() 119 | ]); 120 | return null; // Return null if connection fails 121 | } 122 | }); 123 | 124 | $this->app->singleton('metrics', function () { 125 | $meterProvider = $this->app->make('meterProvider'); 126 | return $meterProvider ? $meterProvider->getMeter('otel_metrics') : null; // Ensure meterProvider is available 127 | }); 128 | } 129 | 130 | /** 131 | * @throws ContainerExceptionInterface 132 | * @throws NotFoundExceptionInterface 133 | */ 134 | private function isOpenTelemetryServerReachable(): bool 135 | { 136 | if (request()->has('otel.server_reachable')) { 137 | return request()->get('otel.server_reachable'); 138 | } 139 | 140 | $endpoint = config('opentelemetry.endpoint'); 141 | if (!filter_var($endpoint, FILTER_VALIDATE_URL)) { 142 | Log::error('Invalid OTEL_EXPORTER_OTLP_ENDPOINT URL provided.'); 143 | request()->merge(['otel.server_reachable' => false]); // Store it in the request 144 | return false; 145 | } 146 | 147 | $host = parse_url($endpoint, PHP_URL_HOST); 148 | $port = parse_url($endpoint, PHP_URL_PORT) ?? 4318; 149 | 150 | $connection = @fsockopen($host, $port, $errno, $err_str, 0.1); 151 | 152 | $reachable = false; 153 | if ($connection) { 154 | fclose($connection); 155 | $reachable = true; 156 | } else { 157 | Log::error('Unreachable OTEL_EXPORTER_OTLP_ENDPOINT URL provided. Error: ' . $err_str); 158 | } 159 | 160 | // Store the result in the request object for this lifecycle 161 | request()->merge(['otel.server_reachable' => $reachable]); 162 | 163 | return $reachable; 164 | } 165 | 166 | public function boot(): void 167 | { 168 | $this->publishes([ 169 | __DIR__ . '/../config/opentelemetry.php' => config_path('opentelemetry.php'), 170 | ]); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Services/MetricsService.php: -------------------------------------------------------------------------------- 1 | metrics = []; 20 | $this->meter = []; 21 | if (app()->bound('metrics')) { 22 | $this->meter = app('metrics'); 23 | } else{ 24 | return null; 25 | } 26 | 27 | $this->initializeMetrics(); 28 | } 29 | 30 | public function initializeMetrics(): void 31 | { 32 | $this->metrics = [ 33 | // HTTP metrics 34 | 'requestCount' => $this->meter->createCounter('http_request_total', ''), 35 | 'statusCodeCount' => $this->meter->createCounter('http_status_code_total', ''), 36 | 'requestLatency' => $this->meter->createHistogram('http_request_latency_seconds', ''), 37 | 'requestSize' => $this->meter->createHistogram('http_request_size_bytes', ''), 38 | 'responseSize' => $this->meter->createHistogram('http_response_size_bytes', ''), 39 | 40 | // System metrics 41 | 'cpuTime' => $this->meter->createCounter('system_cpu_time_seconds_total', ''), 42 | 'memoryUsage' => $this->meter->createGauge('system_memory_usage_bytes', ''), 43 | 'diskUsage' => $this->meter->createHistogram('system_disk_usage_bytes', ''), 44 | 'uptime' => $this->meter->createGauge('application_uptime_seconds', ''), 45 | 46 | // Network metrics 47 | 'networkIO' => $this->meter->createCounter('system_network_io_bytes_total', ''), 48 | 'networkPackets' => $this->meter->createCounter('system_network_packets_total', ''), 49 | 'networkDropped' => $this->meter->createCounter('system_network_dropped_total', ''), 50 | 'networkErrors' => $this->meter->createCounter('system_network_errors_total', ''), 51 | 'networkInbound' => $this->meter->createHistogram('network_inbound_bytes', ''), 52 | 'networkOutbound' => $this->meter->createHistogram('network_outbound_bytes', ''), 53 | 'activeConnections' => $this->meter->createGauge('active_network_connections', ''), 54 | 55 | // Database metrics 56 | 'dbQueryCount' => $this->meter->createCounter('db_query_total', ''), 57 | 'dbQueryLatency' => $this->meter->createHistogram('db_query_latency_seconds', ''), 58 | 'dbErrorCount' => $this->meter->createCounter('db_error_total', ''), 59 | 60 | // Error metric (added) 61 | 'errorCount' => $this->meter->createCounter('error_total', '') 62 | ]; 63 | } 64 | 65 | public function recordMetrics(Request $request, Response $response, $startTime): void 66 | { 67 | $latency = microtime(true) - $startTime; 68 | 69 | $labels = [ 70 | 'method' => $request->method(), 71 | 'route' => $request->path() 72 | ]; 73 | 74 | $statusLabels = array_merge($labels, ['status_code' => $response->getStatusCode()]); 75 | 76 | $this->metrics['requestCount']->add(1, $labels); 77 | $this->metrics['statusCodeCount']->add(1, $statusLabels); 78 | $this->metrics['requestLatency']->record($latency, $labels); 79 | $this->metrics['requestSize']->record(strlen($request->getContent()), $labels); 80 | $this->metrics['responseSize']->record(strlen($response->getContent()), $labels); 81 | } 82 | 83 | public function recordSystemMetrics(): void 84 | { 85 | $cpuStats = $this->getCpuStats(); 86 | foreach ($cpuStats as $state => $time) { 87 | $this->metrics['cpuTime']->add($time, ['state' => $state, 'host' => gethostname()]); 88 | } 89 | 90 | $memoryInfo = $this->getMemoryInfo(); 91 | foreach ($memoryInfo as $state => $value) { 92 | $this->metrics['memoryUsage']->record($value, ['state' => $state, 'host' => gethostname()]); 93 | } 94 | 95 | $diskUsage = disk_free_space(config('opentelemetry.disk_path', '/')); 96 | $this->metrics['diskUsage']->record($diskUsage, ['host' => gethostname()]); 97 | } 98 | 99 | public function recordNetworkMetrics(): void 100 | { 101 | $networkStats = $this->getNetworkStats(); 102 | foreach ($networkStats as $metric => $data) { 103 | foreach ($data as $direction => $value) { 104 | if (isset($this->metrics[$metric])) { 105 | if ($this->metrics[$metric] instanceof Histogram) { 106 | $this->metrics[$metric]->record($value, ['direction' => $direction, 'host' => gethostname()]); 107 | } elseif ($this->metrics[$metric] instanceof Counter) { 108 | $this->metrics[$metric]->add($value, ['direction' => $direction, 'host' => gethostname()]); 109 | } 110 | } 111 | } 112 | } 113 | 114 | $connections = $this->getNetworkConnections(); 115 | foreach ($connections as $state => $count) { 116 | $this->metrics['activeConnections']->record($count, ['state' => $state, 'host' => gethostname()]); 117 | } 118 | } 119 | 120 | public function recordDbMetrics($query): void 121 | { 122 | $executionTime = $query->time / 1000; // Convert milliseconds to seconds 123 | $this->metrics['dbQueryCount']->add(1, ['query' => $query->sql, 'host' => gethostname()]); 124 | $this->metrics['dbQueryLatency']->record($executionTime, ['query' => $query->sql, 'host' => gethostname()]); 125 | 126 | if (isset($query->error)) { 127 | $this->metrics['dbErrorCount']->add(1, ['error' => $query->error, 'host' => gethostname()]); 128 | } 129 | } 130 | 131 | public function wrapCacheOperations(): void 132 | { 133 | Cache::extend('otel', function ($app, $config) { 134 | $store = Cache::driver($config['driver']); 135 | return new class($store, $this->metrics) { 136 | protected $store; 137 | protected $metrics; 138 | 139 | public function __construct($store, $metrics) 140 | { 141 | $this->store = $store; 142 | $this->metrics = $metrics; 143 | } 144 | 145 | public function get($key) 146 | { 147 | $value = $this->store->get($key); 148 | $metric = $value ? 'cacheHitCount' : 'cacheMissCount'; 149 | $this->metrics[$metric]->add(1, ['key' => $key, 'host' => gethostname()]); 150 | return $value; 151 | } 152 | 153 | public function put($key, $value, $ttl = null): void 154 | { 155 | $this->store->put($key, $value, $ttl); 156 | $this->metrics['cacheStoreCount']->add(1, ['key' => $key, 'host' => gethostname()]); 157 | } 158 | 159 | public function forget($key): void 160 | { 161 | $this->store->forget($key); 162 | $this->metrics['cacheDeleteCount']->add(1, ['key' => $key, 'host' => gethostname()]); 163 | } 164 | 165 | public function __call($method, $parameters) 166 | { 167 | return $this->store->$method(...$parameters); 168 | } 169 | }; 170 | }); 171 | } 172 | 173 | public function recordErrorMetrics($e) 174 | { 175 | return $this->metrics['errorCount']->add(1, ['error' => $e->getMessage(), 'host' => gethostname()]); 176 | } 177 | 178 | private function getCpuStats(): array 179 | { 180 | $cpuStates = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal']; 181 | $cpuStats = []; 182 | 183 | if (file_exists(config('opentelemetry.cpu_path', '/proc/stat'))) { 184 | $lines = file(config('opentelemetry.cpu_path', '/proc/stat')); 185 | foreach ($lines as $line) { 186 | if (preg_match('/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) { 187 | foreach ($cpuStates as $index => $state) { 188 | $cpuStats[$state] = (int) $matches[$index + 1]; 189 | } 190 | break; 191 | } 192 | } 193 | } 194 | 195 | return $cpuStats; 196 | } 197 | 198 | private function getMemoryInfo(): array 199 | { 200 | $memoryInfo = []; 201 | 202 | if (file_exists(config('opentelemetry.memory_path', '/proc/meminfo'))) { 203 | $lines = file(config('opentelemetry.memory_path', '/proc/meminfo')); 204 | $rawMemory = []; 205 | foreach ($lines as $line) { 206 | if (preg_match('/^(\w+):\s+(\d+)\s+kB/', $line, $matches)) { 207 | $key = strtolower(str_replace(['(', ')'], '', $matches[1])); 208 | $rawMemory[$key] = (int) $matches[2] / 1024; // Convert KB to MB 209 | } 210 | } 211 | 212 | $memoryInfo = [ 213 | 'buffered' => $rawMemory['buffers'] ?? 0, 214 | 'cached' => $rawMemory['cached'] ?? 0, 215 | 'free' => $rawMemory['memfree'] ?? 0, 216 | 'slab_reclaimable' => $rawMemory['slab_reclaimable'] ?? 0, 217 | 'slab_unreclaimable' => $rawMemory['slab_unreclaimable'] ?? 0, 218 | 'used' => ($rawMemory['memtotal'] ?? 0) - ($rawMemory['memfree'] ?? 0) - ($rawMemory['buffers'] ?? 0) - ($rawMemory['cached'] ?? 0), 219 | ]; 220 | } 221 | 222 | return $memoryInfo; 223 | } 224 | 225 | private function getNetworkConnections(): array 226 | { 227 | $states = config('opentelemetry.network_states', [ 228 | 'ESTABLISHED', 'CLOSE_WAIT', 'TIME_WAIT', 'LISTEN', 'SYN_SENT', 'SYN_RECV', 229 | ]); 230 | $connectionCounts = array_fill_keys($states, 0); 231 | 232 | if (file_exists(config('opentelemetry.connection_path', '/proc/net/tcp'))) { 233 | $lines = file(config('opentelemetry.connection_path', '/proc/net/tcp')); 234 | foreach ($lines as $line) { 235 | foreach ($states as $state) { 236 | if (str_contains($line, $state)) { 237 | $connectionCounts[$state]++; 238 | } 239 | } 240 | } 241 | } 242 | 243 | return $connectionCounts; 244 | } 245 | 246 | private function getNetworkStats(): array 247 | { 248 | $stats = [ 249 | 'networkIO' => ['receive' => 0, 'transmit' => 0], 250 | 'networkPackets' => ['receive' => 0, 'transmit' => 0], 251 | 'networkDropped' => ['receive' => 0, 'transmit' => 0], 252 | 'networkErrors' => ['receive' => 0, 'transmit' => 0], 253 | 'networkInbound' => ['bytes' => 0, 'packets' => 0], 254 | 'networkOutbound' => ['bytes' => 0, 'packets' => 0], 255 | ]; 256 | 257 | $networkPath = config('opentelemetry.network_path', '/proc/net/dev'); 258 | if (!file_exists($networkPath) || !is_readable($networkPath)) { 259 | Log::warning("Network stats file {$networkPath} is not accessible."); 260 | return $stats; 261 | } 262 | 263 | $lines = file($networkPath); 264 | foreach ($lines as $line) { 265 | if (preg_match('/^\s*(?[\w]+):\s*(?\d+)\s+(?\d+)\s+\d+\s+\d+\s+(?\d+)\s+(?\d+)\s+\d+\s*(?\d+)\s+(?\d+)\s+\d+\s+\d+\s+(?\d+)\s+(?\d+)/', $line, $matches)) { 266 | // Aggregate general network stats 267 | $stats['networkIO']['receive'] += (int)$matches['receive_bytes']; 268 | $stats['networkIO']['transmit'] += (int)$matches['transmit_bytes']; 269 | $stats['networkDropped']['receive'] += (int)$matches['receive_dropped']; 270 | $stats['networkDropped']['transmit'] += (int)$matches['transmit_dropped']; 271 | $stats['networkErrors']['receive'] += (int)$matches['receive_errors']; 272 | $stats['networkErrors']['transmit'] += (int)$matches['transmit_errors']; 273 | 274 | // Capture inbound and outbound specific stats 275 | $stats['networkInbound']['bytes'] += (int)$matches['receive_bytes']; 276 | $stats['networkInbound']['packets'] += (int)$matches['receive_packets']; 277 | $stats['networkOutbound']['bytes'] += (int)$matches['transmit_bytes']; 278 | $stats['networkOutbound']['packets'] += (int)$matches['transmit_packets']; 279 | } 280 | } 281 | 282 | return $stats; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Services/TraceService.php: -------------------------------------------------------------------------------- 1 | bound('tracer') ? app('tracer') : null; 16 | } 17 | 18 | public function getCustomTracer() 19 | { 20 | if(app()->bound('tracer')){ 21 | return app('tracer'); 22 | }else{ 23 | die("Unreachable OTEL_EXPORTER_OTLP_ENDPOINT URL provided"); 24 | } 25 | } 26 | 27 | public function dbQueryTrace(): void 28 | { 29 | $tracer = $this->getTracer(); 30 | if (!$tracer) { 31 | return; // Skip if tracer is not available 32 | } 33 | 34 | if (self::$listenerRegistered) { 35 | return; // Skip registering listener if already registered 36 | } 37 | 38 | self::$listenerRegistered = true; 39 | 40 | DB::listen(function ($query) use ($tracer) { 41 | if ($this->shouldExcludeQuery($query->sql)) { 42 | return; // Skip excluded queries 43 | } 44 | 45 | $startTime = (int) (microtime(true) * 1_000_000_000); 46 | 47 | $span = $tracer->spanBuilder('SQL Query: ' . $query->sql) 48 | ->setAttribute('db.system', 'mysql') 49 | ->setAttribute('db.statement', $query->sql) 50 | ->setAttribute('db.bindings', json_encode($query->bindings)) 51 | ->setStartTimestamp($startTime) 52 | ->startSpan(); 53 | 54 | // End the span after execution time 55 | $span->end($startTime + (int) ($query->time * 1_000_000)); 56 | }); 57 | } 58 | 59 | public function setSpanAttributes($span, Request $request, $response): void 60 | { 61 | $span->setAttribute('http.method', $request->method()); 62 | $span->setAttribute('http.url', $request->fullUrl()); 63 | $span->setAttribute('http.route', $this->getRouteName($request)); 64 | $span->setAttribute('http.status_code', $response->getStatusCode()); 65 | $span->setAttribute('http.client_ip', $request->ip()); 66 | $span->setAttribute('http.user_agent', $request->header('User-Agent', 'unknown')); 67 | $span->setAttribute('http.content_length', $request->header('Content-Length', 0)); 68 | 69 | if ($request->user()) { 70 | $span->setAttribute('user.id', $request->user()->id); 71 | } 72 | 73 | $span->setAttribute('response.content_length', strlen($response->getContent() ?? '')); 74 | $span->setAttribute('response.time', microtime(true) - LARAVEL_START); 75 | 76 | $span->addEvent('http.request.received', [ 77 | 'method' => $request->method(), 78 | 'route' => $request->path(), 79 | 'status' => $response->getStatusCode(), 80 | ]); 81 | } 82 | 83 | public function addRouteEvents($span, Request $request, $response, $startTime): void 84 | { 85 | $span->addEvent('request.processed', [ 86 | 'processing_time_ms' => (microtime(true) - $startTime) * 1000, 87 | 'method' => $request->method(), 88 | 'route' => $request->path(), 89 | 'status_code' => $response->getStatusCode(), 90 | ]); 91 | } 92 | 93 | public function handleException($span, Throwable $e): void 94 | { 95 | $span->setAttribute('error', true); 96 | $span->setAttribute('exception.type', get_class($e)); 97 | $span->setAttribute('exception.message', $e->getMessage()); 98 | $span->setAttribute('exception.stacktrace', $e->getTraceAsString()); 99 | $span->addEvent('exception.raised', [ 100 | 'type' => get_class($e), 101 | 'message' => $e->getMessage(), 102 | ]); 103 | } 104 | 105 | private function shouldExcludeQuery(string $sql): bool 106 | { 107 | $excludedQueries = config('opentelemetry.excluded_queries', []); 108 | 109 | foreach ($excludedQueries as $excludedQuery) { 110 | if (str_contains($sql, $excludedQuery)) { 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | private function getRouteName(Request $request): string 119 | { 120 | $route = $request->route(); 121 | return $route ? $route->getName() ?? $route->uri() : 'unknown'; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/config/opentelemetry.php: -------------------------------------------------------------------------------- 1 | env('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317'), 6 | 'protocol' => env('OTEL_EXPORTER_OTLP_PROTOCOL', 'grpc'), 7 | 8 | // Additional configuration for tracing 9 | 'traces_exporter' => env('OTEL_TRACES_EXPORTER', 'otlp'), 10 | 'logs_exporter' => env('OTEL_LOGS_EXPORTER', 'otlp'), 11 | 'metrics_exporter' => env('OTEL_METRICS_EXPORTER', 'otlp'), 12 | 'propagators' => env('OTEL_PROPAGATORS', 'baggage,tracecontext'), 13 | 'traces_sampler' => env('OTEL_TRACES_SAMPLER', 'always_on'), 14 | 15 | // OpenTelemetry service attributes 16 | 'service_name' => env('OTEL_SERVICE_NAME', 'laravel_app'), 17 | 'resource_attributes' => env('OTEL_RESOURCE_ATTRIBUTES', 'deployment.environment=production,service.namespace=default_namespace'), 18 | 19 | // Dynamic route exclusion for tracing and metrics 20 | 'excluded_routes' => [ 21 | 'assets/*', 22 | 'uploads/*', 23 | 'css/*', 24 | 'js/*', 25 | 'images/*', 26 | 'fonts/*', 27 | 'metrics', 28 | 'favicon.ico', 29 | 'api/health' 30 | ], 31 | 32 | // Dynamic queries exclusion for tracing and metrics 33 | 'excluded_queries' => ['select * from `contact_setting` limit 1', 'update `logs`'], 34 | 35 | // System paths for metrics 36 | 'cpu_path' => '/proc/stat', 37 | 'memory_path' => '/proc/meminfo', 38 | 'disk_path' => '/', 39 | 'network_path' => '/proc/net/dev', 40 | 'connection_path' => '/proc/net/tcp', 41 | 'network_states' => ['ESTABLISHED', 'CLOSE_WAIT', 'TIME_WAIT', 'LISTEN', 'SYN_SENT', 'SYN_RECV'], 42 | ]; 43 | --------------------------------------------------------------------------------