├── .idea ├── dictionaries │ └── overtrue.xml ├── vcs.xml ├── modules.xml ├── laravel-open-telemetry.iml └── php.xml ├── src ├── Watchers │ ├── Watcher.php │ ├── ExceptionWatcher.php │ ├── CacheWatcher.php │ ├── RedisWatcher.php │ ├── QueryWatcher.php │ ├── EventWatcher.php │ ├── QueueWatcher.php │ ├── AuthenticateWatcher.php │ └── HttpClientWatcher.php ├── Support │ ├── MeasureDataFlusher.php │ ├── StartedSpan.php │ ├── Metric.php │ ├── SpanBuilder.php │ ├── SpanNameHelper.php │ ├── HttpAttributesHelper.php │ └── Measure.php ├── Handlers │ ├── RequestHandledHandler.php │ ├── RequestReceivedHandler.php │ ├── RequestTerminatedHandler.php │ ├── TickReceivedHandler.php │ ├── WorkerErrorOccurredHandler.php │ ├── TaskReceivedHandler.php │ └── WorkerStartingHandler.php ├── Facades │ ├── Metric.php │ └── Measure.php ├── Http │ └── Middleware │ │ ├── AddTraceId.php │ │ └── TraceRequest.php ├── Traits │ └── InteractWithHttpHeaders.php ├── OpenTelemetryServiceProvider.php └── Console │ └── Commands │ └── TestCommand.php ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── LICENSE ├── composer.json ├── examples ├── duplicate_tracing_test.php ├── simplified_auto_tracing.php ├── test_span_hierarchy.php ├── configuration_guide.php ├── octane_span_hierarchy_test.php ├── improved_measure_usage.php ├── middleware_example.php └── measure_semconv_guide.php ├── config └── otel.php ├── .cursor-rules └── README.md /.idea/dictionaries/overtrue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Watchers/Watcher.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 18 | 'tick.timestamp' => time(), 19 | 'tick.type' => 'scheduled', 20 | ]); 21 | }); 22 | 23 | // Tick events are usually quick, end span immediately 24 | $span->end(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [overtrue] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /src/Handlers/WorkerErrorOccurredHandler.php: -------------------------------------------------------------------------------- 1 | exception) { 22 | $span->recordException($event->exception); 23 | $span->setStatus(StatusCode::STATUS_ERROR, $event->exception->getMessage()); 24 | } 25 | 26 | // Note: Don't end() span here, that's handled uniformly by RequestTerminatedHandler 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Handlers/TaskReceivedHandler.php: -------------------------------------------------------------------------------- 1 | name)); 19 | 20 | $span->setAttributes([ 21 | 'task.name' => $event->name, 22 | 'task.payload' => json_encode($event->payload), 23 | ]); 24 | 25 | // Task completion ends span 26 | // Note: This should call $span->end() after task execution 27 | // But due to Octane's event mechanism, we let it end automatically for now 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | phpcs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup PHP environment 15 | uses: shivammathur/setup-php@v2 16 | - name: Install dependencies 17 | run: composer install --ignore-platform-reqs 18 | - name: Style check 19 | run: composer check-style 20 | phpunit: 21 | strategy: 22 | matrix: 23 | php_version: [8.4] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Setup PHP environment 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php_version }} 31 | coverage: xdebug 32 | extensions: opentelemetry 33 | - name: Install dependencies 34 | run: composer install --ignore-platform-reqs 35 | - name: PHPUnit check 36 | run: ./vendor/bin/phpunit --coverage-text 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 overtrue 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 | -------------------------------------------------------------------------------- /src/Facades/Metric.php: -------------------------------------------------------------------------------- 1 | $var, 42 | ]); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.idea/laravel-open-telemetry.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Watchers/ExceptionWatcher.php: -------------------------------------------------------------------------------- 1 | listen(\Illuminate\Log\Events\MessageLogged::class, [$this, 'recordException']); 25 | } 26 | 27 | public function recordException(MessageLogged $event): void 28 | { 29 | if (! isset($event->context['exception']) || ! ($event->context['exception'] instanceof Throwable)) { 30 | return; 31 | } 32 | 33 | $exception = $event->context['exception']; 34 | $tracer = Measure::tracer(); 35 | 36 | $span = $tracer->spanBuilder(SpanNameHelper::exception(get_class($exception))) 37 | ->setSpanKind(SpanKind::KIND_INTERNAL) 38 | ->setParent(Context::getCurrent()) 39 | ->startSpan(); 40 | 41 | $span->recordException($exception, [ 42 | 'exception.message' => $exception->getMessage(), 43 | 'exception.code' => $exception->getCode(), 44 | ]); 45 | 46 | $span->end(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Watchers/CacheWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CacheHit::class, fn ($event) => $this->recordEvent('cache.hit', $event)); 19 | $app['events']->listen(CacheMissed::class, fn ($event) => $this->recordEvent('cache.miss', $event)); 20 | $app['events']->listen(KeyWritten::class, fn ($event) => $this->recordEvent('cache.set', $event)); 21 | $app['events']->listen(KeyForgotten::class, fn ($event) => $this->recordEvent('cache.forget', $event)); 22 | } 23 | 24 | protected function recordEvent(string $eventName, object $event): void 25 | { 26 | $attributes = [ 27 | 'cache.key' => $event->key, 28 | 'cache.store' => $this->getStoreName($event), 29 | ]; 30 | 31 | if ($event instanceof KeyWritten) { 32 | $attributes['cache.ttl'] = property_exists($event, 'seconds') ? $event->seconds : null; 33 | } 34 | 35 | Measure::addEvent($eventName, $attributes); 36 | } 37 | 38 | private function getStoreName(object $event): ?string 39 | { 40 | return property_exists($event, 'storeName') ? $event->storeName : null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Middleware/AddTraceId.php: -------------------------------------------------------------------------------- 1 | getTraceId(); 24 | 25 | if ($traceId) { 26 | // Get header name from config 27 | $headerName = config('otel.middleware.trace_id.header_name', 'X-Trace-Id'); 28 | 29 | // Add trace ID response header 30 | $response->headers->set($headerName, $traceId); 31 | } 32 | 33 | return $response; 34 | } 35 | 36 | /** 37 | * Get trace ID for current request 38 | */ 39 | protected function getTraceId(): ?string 40 | { 41 | // First try to get from root span 42 | $rootSpan = Measure::getRootSpan(); 43 | if ($rootSpan && $rootSpan->getContext()->isValid()) { 44 | return $rootSpan->getContext()->getTraceId(); 45 | } 46 | 47 | // If no root span, try to get from current active span 48 | $currentSpan = Span::getCurrent(); 49 | if ($currentSpan->getContext()->isValid()) { 50 | return $currentSpan->getContext()->getTraceId(); 51 | } 52 | 53 | return null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overtrue/laravel-open-telemetry", 3 | "description": "This package provides a simple way to add OpenTelemetry to your Laravel application.", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Overtrue\\LaravelOpenTelemetry\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "overtrue", 14 | "email": "anzhengchao@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=8.4", 19 | "laravel/framework": "^10.0|^11.0|^12.0", 20 | "open-telemetry/api": "^1.0", 21 | "open-telemetry/sdk": "^1.0", 22 | "open-telemetry/exporter-otlp": "^1.3", 23 | "open-telemetry/sem-conv": "^1.32" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^9.0", 27 | "laravel/pint": "^1.15", 28 | "spatie/test-time": "^1.3" 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Overtrue\\LaravelOpenTelemetry\\Tests\\": "tests/" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "\\Overtrue\\LaravelOpenTelemetry\\OpenTelemetryServiceProvider" 39 | ] 40 | } 41 | }, 42 | "scripts-descriptions": { 43 | "test": "Run all tests.", 44 | "check-style": "Run style checks (only dry run - no fixing!).", 45 | "fix-style": "Run style checks and fix violations." 46 | }, 47 | "scripts": { 48 | "check-style": "vendor/bin/pint --test", 49 | "fix-style": "vendor/bin/pint", 50 | "test": "phpunit --colors" 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "php-http/discovery": true, 55 | "tbachert/spi": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Watchers/RedisWatcher.php: -------------------------------------------------------------------------------- 1 | listen(CommandExecuted::class, [$this, 'recordCommand']); 24 | } 25 | 26 | public function recordCommand(CommandExecuted $event): void 27 | { 28 | $now = (int) (microtime(true) * 1e9); 29 | $startTime = $now - (int) ($event->time * 1e6); 30 | 31 | $span = Measure::tracer() 32 | ->spanBuilder(SpanNameHelper::redis($event->command)) 33 | ->setSpanKind(SpanKind::KIND_CLIENT) 34 | ->setStartTimestamp($startTime) 35 | ->setParent(Context::getCurrent()) 36 | ->startSpan(); 37 | 38 | $attributes = [ 39 | 'db.system.name' => 'redis', 40 | 'dn.statement' => $this->formatCommand($event->command, $event->parameters), 41 | 'db.connection' => $event->connectionName, 42 | 'db.command.time_ms' => $event->time, 43 | ]; 44 | 45 | $span->setAttributes($attributes)->end($now); 46 | } 47 | 48 | protected function formatCommand(string $command, array $parameters): string 49 | { 50 | $parameters = implode(' ', array_map(fn ($param) => is_string($param) ? (strlen($param) > 100 ? substr($param, 0, 100).'...' : $param) : (is_scalar($param) ? strval($param) : gettype($param)), $parameters)); 51 | 52 | return "{$command} {$parameters}"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Traits/InteractWithHttpHeaders.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | $key = strtolower($key); 19 | $normalized[$key] = is_array($value) ? implode(', ', $value) : (string) $value; 20 | } 21 | 22 | return $normalized; 23 | } 24 | 25 | /** 26 | * Record allowed headers as span attributes. 27 | */ 28 | protected static function recordHeaders(SpanInterface $span, array $headers, string $prefix = 'http.request.header.'): void 29 | { 30 | $normalized = self::normalizeHeaders($headers); 31 | $allowedHeaders = config('otel.allowed_headers', []); 32 | $sensitiveHeaders = config('otel.sensitive_headers', []); 33 | 34 | foreach ($normalized as $key => $value) { 35 | if (self::headerIsAllowed($key, $allowedHeaders)) { 36 | $attributeKey = $prefix.$key; 37 | 38 | if (self::headerIsSensitive($key, $sensitiveHeaders)) { 39 | $span->setAttribute($attributeKey, '***'); 40 | } else { 41 | $span->setAttribute($attributeKey, $value); 42 | } 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Check if header is allowed. 49 | */ 50 | protected static function headerIsAllowed(string $header, array $allowedHeaders): bool 51 | { 52 | return array_any($allowedHeaders, fn ($pattern) => fnmatch($pattern, $header)); 53 | } 54 | 55 | /** 56 | * Check if header is sensitive. 57 | */ 58 | protected static function headerIsSensitive(string $header, array $sensitiveHeaders): bool 59 | { 60 | return array_any($sensitiveHeaders, fn ($pattern) => fnmatch($pattern, $header)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Facades/Measure.php: -------------------------------------------------------------------------------- 1 | listen(QueryExecuted::class, [$this, 'recordQuery']); 21 | } 22 | 23 | public function recordQuery(QueryExecuted $event): void 24 | { 25 | $now = (int) (microtime(true) * 1e9); 26 | $startTime = $now - (int) ($event->time * 1e6); 27 | 28 | $span = Measure::tracer() 29 | ->spanBuilder(SpanNameHelper::database($this->getOperationName($event->sql), $this->extractTableName($event->sql))) 30 | ->setSpanKind(SpanKind::KIND_INTERNAL) 31 | ->setStartTimestamp($startTime) 32 | ->setParent(Context::getCurrent()) 33 | ->startSpan(); 34 | 35 | $span->setAttributes([ 36 | TraceAttributes::DB_SYSTEM => $event->connection->getDriverName(), 37 | TraceAttributes::DB_NAME => $event->connection->getDatabaseName(), 38 | TraceAttributes::DB_STATEMENT => $event->sql, 39 | 'db.connection' => $event->connectionName, 40 | 'db.query.time_ms' => $event->time, 41 | ]); 42 | 43 | $span->end($now); 44 | } 45 | 46 | protected function getOperationName(string $sql): string 47 | { 48 | $name = Str::upper(Str::before($sql, ' ')); 49 | 50 | return in_array($name, ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP', 'TRUNCATE']) ? $name : 'QUERY'; 51 | } 52 | 53 | protected function extractTableName(string $sql): ?string 54 | { 55 | if (preg_match('/(?:from|into|update|join|table)\s+[`"\']?(\w+)[`"\']?/i', $sql, $matches)) { 56 | return $matches[1]; 57 | } 58 | 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Http/Middleware/TraceRequest.php: -------------------------------------------------------------------------------- 1 | headers->all()); 31 | 32 | $span = Measure::startRootSpan(SpanNameHelper::http($request), [], $parentContext); 33 | 34 | try { 35 | // Set request attributes 36 | HttpAttributesHelper::setRequestAttributes($span, $request); 37 | 38 | // Process request 39 | $response = $next($request); 40 | 41 | // Set response attributes and status 42 | HttpAttributesHelper::setResponseAttributes($span, $response); 43 | HttpAttributesHelper::setSpanStatusFromResponse($span, $response); 44 | 45 | // Add trace ID to response headers 46 | HttpAttributesHelper::addTraceIdToResponse($span, $response); 47 | 48 | return $response; 49 | 50 | } catch (Throwable $exception) { 51 | // Log exception for debugging purposes 52 | Log::error('[laravel-open-telemetry] TraceRequest: Exception occurred during request processing', [ 53 | 'exception' => $exception->getMessage(), 54 | 'file' => $exception->getFile(), 55 | 'line' => $exception->getLine(), 56 | 'trace_id' => $span->getContext()->getTraceId(), 57 | ]); 58 | 59 | // Record exception 60 | $span->recordException($exception); 61 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 62 | 63 | throw $exception; 64 | } finally { 65 | // End span and detach scope 66 | Measure::endRootSpan(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Watchers/EventWatcher.php: -------------------------------------------------------------------------------- 1 | listen('*', [$this, 'recordEvent']); 48 | } 49 | 50 | public function recordEvent($eventName, $payload = []): void 51 | { 52 | if ($this->shouldSkip($eventName)) { 53 | return; 54 | } 55 | 56 | $attributes = [ 57 | 'event.payload_count' => is_array($payload) ? count($payload) : 0, 58 | ]; 59 | 60 | $firstPayload = is_array($payload) ? ($payload[0] ?? null) : null; 61 | if (is_object($firstPayload)) { 62 | $attributes['event.object_type'] = get_class($firstPayload); 63 | } 64 | 65 | Measure::addEvent($eventName, $attributes); 66 | } 67 | 68 | protected function shouldSkip(string $eventName): bool 69 | { 70 | if (str_starts_with($eventName, 'otel.') || str_starts_with($eventName, 'opentelemetry.')) { 71 | return true; 72 | } 73 | 74 | return in_array($eventName, $this->eventsToSkip); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/duplicate_tracing_test.php: -------------------------------------------------------------------------------- 1 | [\n"; 48 | echo " 'propagation_middleware' => ['enabled' => false]\n"; 49 | echo "]\n\n"; 50 | 51 | echo "Best Practices:\n"; 52 | echo "1. Let HttpClientWatcher handle all HTTP request tracing automatically\n"; 53 | echo "2. Use Measure::trace() for custom business logic spans\n"; 54 | echo "3. No need to manually add tracing to HTTP client calls\n"; 55 | echo "4. Context propagation happens automatically across services\n\n"; 56 | 57 | echo "Result:\n"; 58 | echo "Every HTTP request is traced exactly once with proper context propagation\n"; 59 | echo "between microservices - completely automatically!\n"; 60 | -------------------------------------------------------------------------------- /examples/simplified_auto_tracing.php: -------------------------------------------------------------------------------- 1 | 'test']); // EventWatcher automatically traces 19 | Cache::get('foo', 'bar'); // CacheWatcher automatically traces 20 | 21 | return 'Hello, World!'; 22 | }); 23 | 24 | Route::get('/foo', function () { 25 | return Measure::trace('hello2', function () { 26 | sleep(rand(1, 3)); 27 | Http::get('https://httpbin.org/ip'); // Automatically traced 28 | event('hello.created2', ['name' => 'test']); // EventWatcher automatically traces 29 | Cache::get('foo', 'bar'); // CacheWatcher automatically traces 30 | 31 | return 'Hello, Foo!'; 32 | }); 33 | }); 34 | 35 | Route::get('/trace-test', function () { 36 | $tracer = Measure::tracer(); 37 | 38 | $span = $tracer->spanBuilder('test span') 39 | ->setAttribute('test.attribute', 'value') 40 | ->startSpan(); 41 | 42 | sleep(rand(1, 3)); 43 | 44 | // ✅ Automatic tracing: HttpClientWatcher handles all requests automatically 45 | Http::get('http://127.0.0.1:8002/foo'); // HttpClientWatcher handles automatically 46 | event('hello.created', ['name' => 'test']); // EventWatcher handles automatically 47 | Cache::get('foo', 'bar'); // CacheWatcher handles automatically 48 | 49 | $span->end(); 50 | 51 | $span1 = $tracer->spanBuilder('test span 1') 52 | ->setAttribute('test.attribute.1', 'value.1') 53 | ->startSpan(); 54 | $span1->end(); 55 | 56 | return [ 57 | 'span_id' => $span->getContext()->getSpanId(), 58 | 'trace_id' => $span->getContext()->getTraceId(), 59 | 'trace_flags' => $span->getContext()->getTraceFlags(), 60 | 'trace_state' => $span->getContext()->getTraceState(), 61 | 'span_name' => $span->getName(), 62 | 'env' => array_filter($_ENV, function ($key) { 63 | return str_starts_with($key, 'OTEL_') || str_starts_with($key, 'OTEL_EXPORTER_'); 64 | }, ARRAY_FILTER_USE_KEY), 65 | 'url' => sprintf('http://localhost:16686/jaeger/ui/trace/%s', $span->getContext()->getTraceId()), 66 | ]; 67 | }); 68 | -------------------------------------------------------------------------------- /examples/test_span_hierarchy.php: -------------------------------------------------------------------------------- 1 | singleton(\Overtrue\LaravelOpenTelemetry\Support\Measure::class, function ($app) { 17 | return new \Overtrue\LaravelOpenTelemetry\Support\Measure($app); 18 | }); 19 | 20 | echo "=== Testing Span Hierarchy ===\n\n"; 21 | 22 | // 1. Create root span (simulate HTTP request) 23 | echo "1. Creating root span\n"; 24 | $rootSpan = Measure::startRootSpan('GET /api/users', [ 25 | 'http.method' => 'GET', 26 | 'http.url' => '/api/users', 27 | 'span.kind' => 'server', 28 | ]); 29 | echo 'Root span ID: '.$rootSpan->getContext()->getSpanId()."\n"; 30 | echo 'Trace ID: '.$rootSpan->getContext()->getTraceId()."\n\n"; 31 | 32 | // 2. Create child span (simulate database query) 33 | echo "2. Creating database query span\n"; 34 | $dbSpan = Measure::span('db.query', 'users') 35 | ->setSpanKind(SpanKind::KIND_CLIENT) 36 | ->setAttribute('db.statement', 'SELECT * FROM users') 37 | ->setAttribute('db.operation', 'SELECT') 38 | ->startAndActivate(); 39 | 40 | echo 'Database span ID: '.$dbSpan->getSpan()->getContext()->getSpanId()."\n"; 41 | echo 'Parent span ID: '.$rootSpan->getContext()->getSpanId()."\n"; 42 | echo 'Same Trace ID: '.($dbSpan->getSpan()->getContext()->getTraceId() === $rootSpan->getContext()->getTraceId() ? 'Yes' : 'No')."\n\n"; 43 | 44 | // 3. Create nested child span (simulate cache operation) 45 | echo "3. Creating cache operation span\n"; 46 | $cacheSpan = Measure::span('cache.get', 'users') 47 | ->setSpanKind(SpanKind::KIND_CLIENT) 48 | ->setAttribute('cache.key', 'users:all') 49 | ->setAttribute('cache.operation', 'get') 50 | ->startAndActivate(); 51 | 52 | echo 'Cache span ID: '.$cacheSpan->getSpan()->getContext()->getSpanId()."\n"; 53 | echo 'Parent span ID: '.$dbSpan->getSpan()->getContext()->getSpanId()."\n"; 54 | echo 'Same Trace ID: '.($cacheSpan->getSpan()->getContext()->getTraceId() === $rootSpan->getContext()->getTraceId() ? 'Yes' : 'No')."\n\n"; 55 | 56 | // 4. End spans in correct order 57 | echo "4. Ending spans\n"; 58 | $cacheSpan->end(); 59 | echo "Cache span ended\n"; 60 | 61 | $dbSpan->end(); 62 | echo "Database span ended\n"; 63 | 64 | Measure::endRootSpan(); 65 | echo "Root span ended\n\n"; 66 | 67 | echo "=== Span Hierarchy Test Complete ===\n"; 68 | echo "If all spans have the same Trace ID, the span chain is working correctly!\n"; 69 | -------------------------------------------------------------------------------- /src/Support/StartedSpan.php: -------------------------------------------------------------------------------- 1 | span; 22 | } 23 | 24 | public function getScope(): ScopeInterface 25 | { 26 | return $this->scope; 27 | } 28 | 29 | public function setAttribute(string $key, mixed $value): self 30 | { 31 | if ($this->ended) { 32 | return $this; // Silently ignore if already ended 33 | } 34 | 35 | $this->span->setAttribute($key, $value); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @param array $attributes 42 | */ 43 | public function setAttributes(array $attributes): self 44 | { 45 | if ($this->ended) { 46 | return $this; // Silently ignore if already ended 47 | } 48 | 49 | $this->span->setAttributes($attributes); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param array $attributes 56 | */ 57 | public function addEvent(string $name, array $attributes = [], ?int $timestamp = null): self 58 | { 59 | if ($this->ended) { 60 | return $this; // Silently ignore if already ended 61 | } 62 | 63 | $this->span->addEvent($name, $attributes, $timestamp); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @param array $attributes 70 | */ 71 | public function recordException(\Throwable $exception, array $attributes = []): self 72 | { 73 | if ($this->ended) { 74 | return $this; // Silently ignore if already ended 75 | } 76 | 77 | $this->span->recordException($exception, $attributes); 78 | 79 | return $this; 80 | } 81 | 82 | public function isEnded(): bool 83 | { 84 | return $this->ended; 85 | } 86 | 87 | public function end(?int $endEpochNanos = null): void 88 | { 89 | if ($this->ended) { 90 | return; // Prevent double-ending 91 | } 92 | 93 | $this->span->end($endEpochNanos); 94 | 95 | try { 96 | $this->scope->detach(); 97 | } catch (\Throwable $e) { 98 | // Scope may already be detached, ignore silently 99 | } 100 | 101 | $this->ended = true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/configuration_guide.php: -------------------------------------------------------------------------------- 1 | [\n"; 71 | echo " 'propagation_middleware' => [\n"; 72 | echo " 'enabled' => false,\n"; 73 | echo " ],\n"; 74 | echo "],\n"; 75 | -------------------------------------------------------------------------------- /src/Support/Metric.php: -------------------------------------------------------------------------------- 1 | forceFlush(); 49 | } 50 | 51 | // ======================= Core OpenTelemetry API ======================= 52 | 53 | /** 54 | * Get the meter instance 55 | */ 56 | public function meter(): MeterInterface 57 | { 58 | if (! $this->isEnabled()) { 59 | return new NoopMeter; 60 | } 61 | 62 | try { 63 | return $this->app->get(MeterInterface::class); 64 | } catch (Throwable $e) { 65 | Log::error('[laravel-open-telemetry] Meter not found', [ 66 | 'error' => $e->getMessage(), 67 | 'line' => $e->getLine(), 68 | 'file' => $e->getFile(), 69 | ]); 70 | 71 | return new NoopMeter; 72 | } 73 | } 74 | 75 | public function counter(string $name, ?string $unit = null, 76 | ?string $description = null, array $advisories = []): CounterInterface 77 | { 78 | return $this->meter()->createCounter($name, $unit, $description, $advisories); 79 | } 80 | 81 | public function histogram(string $name, ?string $unit = null, 82 | ?string $description = null, array $advisories = []): HistogramInterface 83 | { 84 | return $this->meter()->createHistogram($name, $unit, $description, $advisories); 85 | } 86 | 87 | public function gauge(string $name, ?string $unit = null, 88 | ?string $description = null, array $advisories = []): GaugeInterface 89 | { 90 | return $this->meter()->createGauge($name, $unit, $description, $advisories); 91 | } 92 | 93 | public function observableGauge(string $name, ?string $unit = null, 94 | ?string $description = null, array|callable $advisories = [], callable ...$callbacks): ObservableGaugeInterface 95 | { 96 | return $this->meter()->createObservableGauge($name, $unit, $description, $advisories, ...$callbacks); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /examples/octane_span_hierarchy_test.php: -------------------------------------------------------------------------------- 1 | count(); // QueryWatcher 应该创建子 span 21 | 22 | // 2. 测试缓存操作 span 层次结构 23 | Cache::remember('test_key', 60, function () { 24 | return 'cached_value'; 25 | }); // CacheWatcher 应该创建子 span 26 | 27 | // 3. 测试 HTTP 客户端 span 层次结构 28 | Http::get('https://httpbin.org/ip'); // HttpClientWatcher 应该创建子 span 29 | 30 | // 4. 触发事件测试 EventWatcher 31 | event('test.event', ['data' => 'test']); // EventWatcher 应该创建子 span 32 | 33 | // 5. 嵌套操作测试 34 | return Measure::trace('nested_operation', function () { 35 | // 这些操作应该都是 nested_operation 的子 span 36 | DB::table('posts')->where('status', 'published')->count(); 37 | Cache::get('another_key', 'default'); 38 | Http::get('https://httpbin.org/uuid'); 39 | 40 | return [ 41 | 'message' => '所有操作应该正确维持层次结构', 42 | 'trace_id' => Measure::traceId(), 43 | 'expected_structure' => [ 44 | 'main_operation' => [ 45 | 'database.query (users)', 46 | 'cache.set (test_key)', 47 | 'http.client.get (httpbin.org/ip)', 48 | 'event (test.event)', 49 | 'nested_operation' => [ 50 | 'database.query (posts)', 51 | 'cache.miss (another_key)', 52 | 'http.client.get (httpbin.org/uuid)', 53 | ], 54 | ], 55 | ], 56 | ]; 57 | }); 58 | }); 59 | }); 60 | 61 | Route::get('/octane-context-test', function () { 62 | // 测试在 Octane 长期运行进程中 context 是否正确传播 63 | $results = []; 64 | 65 | // 模拟多个并发请求情况 66 | for ($i = 1; $i <= 3; $i++) { 67 | $traceResult = Measure::trace("request_{$i}", function () use ($i) { 68 | // 每个请求都应该有独立的 trace 69 | DB::select("SELECT {$i} as request_id"); 70 | Cache::put("request_{$i}", $i, 60); 71 | 72 | return [ 73 | 'request_id' => $i, 74 | 'trace_id' => Measure::traceId(), 75 | 'span_count' => 'should_include_db_and_cache_spans', 76 | ]; 77 | }); 78 | 79 | $results[] = $traceResult; 80 | } 81 | 82 | return [ 83 | 'message' => '每个请求应该有独立的 trace_id,但 spans 应该正确关联', 84 | 'results' => $results, 85 | 'verification' => [ 86 | 'each_request_has_unique_trace_id' => true, 87 | 'spans_are_properly_nested' => true, 88 | 'no_orphaned_spans' => true, 89 | ], 90 | ]; 91 | }); 92 | -------------------------------------------------------------------------------- /src/Support/SpanBuilder.php: -------------------------------------------------------------------------------- 1 | builder->setParent($context); 19 | 20 | return $this; 21 | } 22 | 23 | public function addLink(SpanInterface $span, array $attributes = []): self 24 | { 25 | $this->builder->addLink($span->getContext(), $attributes); 26 | 27 | return $this; 28 | } 29 | 30 | public function setAttribute(string $key, mixed $value): self 31 | { 32 | $this->builder->setAttribute($key, $value); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param array $attributes 39 | */ 40 | public function setAttributes(array $attributes): self 41 | { 42 | $this->builder->setAttributes($attributes); 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Set the start timestamp in nanoseconds 49 | */ 50 | public function setStartTimestamp(int $timestampNanos): self 51 | { 52 | $this->builder->setStartTimestamp($timestampNanos); 53 | 54 | return $this; 55 | } 56 | 57 | public function setSpanKind(int $spanKind): self 58 | { 59 | $this->builder->setSpanKind($spanKind); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Start a span without activating its scope 66 | * This is the new default behavior - more predictable and safer 67 | */ 68 | public function start(): SpanInterface 69 | { 70 | return $this->builder->startSpan(); 71 | } 72 | 73 | /** 74 | * Start a span and activate its scope 75 | * Use this when you need the span to be active in the current context 76 | */ 77 | public function startAndActivate(): StartedSpan 78 | { 79 | $span = $this->builder->startSpan(); 80 | 81 | // Store span in context and activate 82 | $spanContext = $span->storeInContext(Context::getCurrent()); 83 | $scope = $spanContext->activate(); 84 | 85 | return new StartedSpan($span, $scope); 86 | } 87 | 88 | /** 89 | * Start a span without activating its scope 90 | * Alias for start() method for clarity 91 | */ 92 | public function startSpan(): SpanInterface 93 | { 94 | return $this->start(); 95 | } 96 | 97 | /** 98 | * Start a span and store it in context without activating scope 99 | * Returns both the span and the context for manual scope management 100 | */ 101 | public function startWithContext(): array 102 | { 103 | $span = $this->builder->startSpan(); 104 | $context = $span->storeInContext(Context::getCurrent()); 105 | 106 | return [$span, $context]; 107 | } 108 | 109 | /** 110 | * @throws \Throwable 111 | */ 112 | public function measure(\Closure $callback): mixed 113 | { 114 | $span = $this->startAndActivate(); 115 | 116 | try { 117 | return $callback($span->getSpan()); 118 | } catch (\Throwable $exception) { 119 | $span->recordException($exception); 120 | throw $exception; 121 | } finally { 122 | $span->end(); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Watchers/QueueWatcher.php: -------------------------------------------------------------------------------- 1 | listen(JobQueued::class, [$this, 'recordJobQueued']); 26 | $app['events']->listen(JobProcessing::class, [$this, 'recordJobProcessing']); 27 | $app['events']->listen(JobProcessed::class, [$this, 'recordJobProcessed']); 28 | $app['events']->listen(JobFailed::class, [$this, 'recordJobFailed']); 29 | } 30 | 31 | public function recordJobQueued(JobQueued $event): void 32 | { 33 | $jobClass = is_object($event->job) ? get_class($event->job) : $event->job; 34 | 35 | $attributes = [ 36 | TraceAttributes::MESSAGING_SYSTEM => $event->connectionName, 37 | TraceAttributes::MESSAGING_DESTINATION_NAME => $event->queue, 38 | TraceAttributes::MESSAGING_MESSAGE_ID => $event->id, 39 | 'messaging.job.class' => $jobClass, 40 | ]; 41 | 42 | if (is_object($event->job) && method_exists($event->job, 'delay') && $event->job->delay) { 43 | $attributes['messaging.job.delay_seconds'] = $event->job->delay; 44 | } 45 | 46 | Measure::addEvent('queue.job.queued', $attributes); 47 | } 48 | 49 | public function recordJobProcessing(JobProcessing $event): void 50 | { 51 | $payload = $event->job->payload(); 52 | $jobClass = $payload['displayName'] ?? 'unknown'; 53 | 54 | $attributes = [ 55 | TraceAttributes::MESSAGING_SYSTEM => $event->connectionName, 56 | TraceAttributes::MESSAGING_DESTINATION_NAME => $event->job->getQueue(), 57 | TraceAttributes::MESSAGING_MESSAGE_ID => $event->job->getJobId(), 58 | 'messaging.job.class' => $jobClass, 59 | 'messaging.job.attempts' => $event->job->attempts(), 60 | 'messaging.job.max_tries' => $payload['maxTries'] ?? null, 61 | 'messaging.job.timeout' => $payload['timeout'] ?? null, 62 | ]; 63 | 64 | if (isset($payload['data'])) { 65 | $attributes['messaging.job.data_size'] = strlen(serialize($payload['data'])); 66 | } 67 | 68 | Measure::addEvent('queue.job.processing', $attributes); 69 | } 70 | 71 | public function recordJobProcessed(JobProcessed $event): void 72 | { 73 | $jobClass = $event->job->payload()['displayName'] ?? 'unknown'; 74 | 75 | Measure::addEvent('queue.job.processed', [ 76 | 'messaging.job.id' => $event->job->getJobId(), 77 | 'messaging.job.class' => $jobClass, 78 | 'messaging.job.status' => 'completed', 79 | ]); 80 | 81 | MeasureDataFlusher::flush(); 82 | } 83 | 84 | public function recordJobFailed(JobFailed $event): void 85 | { 86 | $jobClass = $event->job->payload()['displayName'] ?? 'unknown'; 87 | 88 | Measure::recordException($event->exception, [ 89 | 'messaging.job.id' => $event->job->getJobId(), 90 | 'messaging.job.class' => $jobClass, 91 | 'messaging.job.status' => 'failed', 92 | ]); 93 | 94 | MeasureDataFlusher::flush(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Support/SpanNameHelper.php: -------------------------------------------------------------------------------- 1 | path()) { 17 | // Use route pattern, more intuitive 18 | return sprintf('HTTP %s %s', $request->method(), $route); 19 | } 20 | 21 | // Fallback to actual path 22 | return sprintf('HTTP %s /%s', $request->method(), $request->path()); 23 | } 24 | 25 | /** 26 | * Generate span name for HTTP client requests 27 | * Format: HTTP {METHOD} {hostname}{path} 28 | */ 29 | public static function httpClient(string $method, string $url): string 30 | { 31 | $parsedUrl = parse_url($url); 32 | $host = $parsedUrl['host'] ?? 'unknown'; 33 | $path = $parsedUrl['path'] ?? '/'; 34 | 35 | return sprintf('HTTP %s %s%s', strtoupper($method), $host, $path); 36 | } 37 | 38 | /** 39 | * Generate span name for database queries 40 | * Format: DB {operation} {table} 41 | */ 42 | public static function database(string $operation, ?string $table = null): string 43 | { 44 | if ($table) { 45 | return sprintf('DB %s %s', strtoupper($operation), $table); 46 | } 47 | 48 | return sprintf('DB %s', strtoupper($operation)); 49 | } 50 | 51 | /** 52 | * Generate span name for Redis commands 53 | * Format: REDIS {command} 54 | */ 55 | public static function redis(string $command): string 56 | { 57 | return sprintf('REDIS %s', strtoupper($command)); 58 | } 59 | 60 | /** 61 | * Generate span name for queue jobs 62 | * Format: QUEUE {operation} {job class name} 63 | */ 64 | public static function queue(string $operation, ?string $jobClass = null): string 65 | { 66 | if ($jobClass) { 67 | // Extract class name (remove namespace) 68 | $className = class_basename($jobClass); 69 | 70 | return sprintf('QUEUE %s %s', strtoupper($operation), $className); 71 | } 72 | 73 | return sprintf('QUEUE %s', strtoupper($operation)); 74 | } 75 | 76 | /** 77 | * Generate span name for authentication operations 78 | * Format: AUTH {operation} 79 | */ 80 | public static function auth(string $operation): string 81 | { 82 | return sprintf('AUTH %s', strtoupper($operation)); 83 | } 84 | 85 | /** 86 | * Generate span name for cache operations 87 | * Format: CACHE {operation} {key} 88 | */ 89 | public static function cache(string $operation, ?string $key = null): string 90 | { 91 | if ($key) { 92 | // Limit key length to avoid overly long span names 93 | $shortKey = strlen($key) > 50 ? substr($key, 0, 47).'...' : $key; 94 | 95 | return sprintf('CACHE %s %s', strtoupper($operation), $shortKey); 96 | } 97 | 98 | return sprintf('CACHE %s', strtoupper($operation)); 99 | } 100 | 101 | /** 102 | * Generate span name for events 103 | * Format: EVENT {event name} 104 | */ 105 | public static function event(string $eventName): string 106 | { 107 | // Simplify event name, remove namespace prefix 108 | $shortEventName = str_replace(['Illuminate\\', 'App\\Events\\'], '', $eventName); 109 | 110 | return sprintf('EVENT %s', $shortEventName); 111 | } 112 | 113 | /** 114 | * Generate span name for exception handling 115 | * Format: EXCEPTION {exception class name} 116 | */ 117 | public static function exception(string $exceptionClass): string 118 | { 119 | $className = class_basename($exceptionClass); 120 | 121 | return sprintf('EXCEPTION %s', $className); 122 | } 123 | 124 | /** 125 | * Generate span name for console commands 126 | * Format: COMMAND {command name} 127 | */ 128 | public static function command(string $commandName): string 129 | { 130 | return sprintf('COMMAND %s', $commandName); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Watchers/AuthenticateWatcher.php: -------------------------------------------------------------------------------- 1 | listen(Attempting::class, [$this, 'recordAttempting']); 29 | $app['events']->listen(Authenticated::class, [$this, 'recordAuthenticated']); 30 | $app['events']->listen(Login::class, [$this, 'recordLogin']); 31 | $app['events']->listen(Failed::class, [$this, 'recordFailed']); 32 | $app['events']->listen(Logout::class, [$this, 'recordLogout']); 33 | } 34 | 35 | public function recordAttempting(Attempting $event): void 36 | { 37 | $span = Measure::tracer() 38 | ->spanBuilder(SpanNameHelper::auth('attempting')) 39 | ->setSpanKind(SpanKind::KIND_INTERNAL) 40 | ->setParent(Context::getCurrent()) 41 | ->startSpan(); 42 | 43 | $span->setAttributes([ 44 | 'auth.guard' => $event->guard, 45 | 'auth.credentials.count' => count($event->credentials), 46 | 'auth.remember' => $event->remember, 47 | ]); 48 | 49 | $span->end(); 50 | } 51 | 52 | public function recordAuthenticated(Authenticated $event): void 53 | { 54 | $span = Measure::tracer() 55 | ->spanBuilder(SpanNameHelper::auth('authenticated')) 56 | ->setSpanKind(SpanKind::KIND_INTERNAL) 57 | ->setParent(Context::getCurrent()) 58 | ->startSpan(); 59 | 60 | $span->setAttributes([ 61 | 'auth.guard' => $event->guard, 62 | TraceAttributes::ENDUSER_ID => $event->user->getAuthIdentifier(), 63 | 'auth.user.type' => get_class($event->user), 64 | ]); 65 | 66 | $span->end(); 67 | } 68 | 69 | public function recordLogin(Login $event): void 70 | { 71 | $span = Measure::tracer() 72 | ->spanBuilder(SpanNameHelper::auth('login')) 73 | ->setSpanKind(SpanKind::KIND_INTERNAL) 74 | ->setParent(Context::getCurrent()) 75 | ->startSpan(); 76 | 77 | $span->setAttributes([ 78 | 'auth.guard' => $event->guard, 79 | TraceAttributes::ENDUSER_ID => $event->user->getAuthIdentifier(), 80 | 'auth.user.type' => get_class($event->user), 81 | 'auth.remember' => $event->remember, 82 | ]); 83 | 84 | $span->end(); 85 | } 86 | 87 | public function recordFailed(Failed $event): void 88 | { 89 | $span = Measure::tracer() 90 | ->spanBuilder(SpanNameHelper::auth('failed')) 91 | ->setSpanKind(SpanKind::KIND_INTERNAL) 92 | ->setParent(Context::getCurrent()) 93 | ->startSpan(); 94 | 95 | $span->setAttributes([ 96 | 'auth.guard' => $event->guard, 97 | 'auth.credentials.count' => count($event->credentials), 98 | TraceAttributes::ENDUSER_ID => $event->user?->getAuthIdentifier(), 99 | ]); 100 | 101 | $span->end(); 102 | } 103 | 104 | public function recordLogout(Logout $event): void 105 | { 106 | $span = Measure::tracer() 107 | ->spanBuilder(SpanNameHelper::auth('logout')) 108 | ->setSpanKind(SpanKind::KIND_INTERNAL) 109 | ->setParent(Context::getCurrent()) 110 | ->startSpan(); 111 | 112 | $span->setAttributes([ 113 | 'auth.guard' => $event->guard, 114 | TraceAttributes::ENDUSER_ID => $event->user?->getAuthIdentifier(), 115 | 'auth.user.type' => $event->user ? get_class($event->user) : null, 116 | ]); 117 | 118 | $span->end(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /config/otel.php: -------------------------------------------------------------------------------- 1 | env('OTEL_ENABLED', false), 10 | 11 | /** 12 | * The name of the tracer that will be used to create spans. 13 | * This is useful for identifying the source of the spans. 14 | */ 15 | 'tracer_name' => env('OTEL_TRACER_NAME', 'overtrue.laravel-open-telemetry'), 16 | 17 | /** 18 | * Middleware Configuration 19 | */ 20 | 'middleware' => [ 21 | /** 22 | * Trace ID Middleware Configuration 23 | * Used to add X-Trace-Id to response headers 24 | */ 25 | 'trace_id' => [ 26 | 'enabled' => env('OTEL_TRACE_ID_MIDDLEWARE_ENABLED', true), 27 | 'global' => env('OTEL_TRACE_ID_MIDDLEWARE_GLOBAL', true), 28 | 'header_name' => env('OTEL_TRACE_ID_HEADER_NAME', 'X-Trace-Id'), 29 | ], 30 | ], 31 | 32 | /** 33 | * HTTP Client Configuration 34 | */ 35 | 'http_client' => [ 36 | /** 37 | * Global Request Middleware Configuration 38 | * Automatically adds OpenTelemetry propagation headers to all HTTP requests 39 | */ 40 | 'propagation_middleware' => [ 41 | 'enabled' => env('OTEL_HTTP_CLIENT_PROPAGATION_ENABLED', true), 42 | ], 43 | ], 44 | 45 | /** 46 | * Watchers Configuration 47 | * 48 | * Available Watcher classes: 49 | * - \Overtrue\LaravelOpenTelemetry\Watchers\CacheWatcher::class 50 | * - \Overtrue\LaravelOpenTelemetry\Watchers\QueryWatcher::class 51 | * - \Overtrue\LaravelOpenTelemetry\Watchers\HttpClientWatcher::class 52 | * - \Overtrue\LaravelOpenTelemetry\Watchers\ExceptionWatcher::class 53 | * - \Overtrue\LaravelOpenTelemetry\Watchers\AuthenticateWatcher::class 54 | * - \Overtrue\LaravelOpenTelemetry\Watchers\EventWatcher::class 55 | * - \Overtrue\LaravelOpenTelemetry\Watchers\QueueWatcher::class 56 | * - \Overtrue\LaravelOpenTelemetry\Watchers\RedisWatcher::class 57 | */ 58 | 'watchers' => [ 59 | \Overtrue\LaravelOpenTelemetry\Watchers\CacheWatcher::class, 60 | \Overtrue\LaravelOpenTelemetry\Watchers\QueryWatcher::class, 61 | \Overtrue\LaravelOpenTelemetry\Watchers\HttpClientWatcher::class, // 已添加智能重复检测,可以同时使用 62 | \Overtrue\LaravelOpenTelemetry\Watchers\ExceptionWatcher::class, 63 | \Overtrue\LaravelOpenTelemetry\Watchers\AuthenticateWatcher::class, 64 | \Overtrue\LaravelOpenTelemetry\Watchers\EventWatcher::class, 65 | \Overtrue\LaravelOpenTelemetry\Watchers\QueueWatcher::class, 66 | \Overtrue\LaravelOpenTelemetry\Watchers\RedisWatcher::class, 67 | ], 68 | 69 | /** 70 | * Allow to trace requests with specific headers. You can use `*` as wildcard. 71 | */ 72 | 'allowed_headers' => explode(',', env('OTEL_ALLOWED_HEADERS', implode(',', [ 73 | 'referer', 74 | 'x-*', 75 | 'accept', 76 | 'request-id', 77 | ]))), 78 | 79 | /** 80 | * Sensitive headers will be marked as *** from the span attributes. You can use `*` as wildcard. 81 | */ 82 | 'sensitive_headers' => explode(',', env('OTEL_SENSITIVE_HEADERS', implode(',', [ 83 | 'cookie', 84 | 'authorization', 85 | 'x-api-key', 86 | ]))), 87 | 88 | /** 89 | * Ignore paths will not be traced. You can use `*` as wildcard. 90 | */ 91 | 'ignore_paths' => explode(',', env('OTEL_IGNORE_PATHS', implode(',', [ 92 | 'up', 93 | 'horizon*', // Laravel Horizon dashboard 94 | 'telescope*', // Laravel Telescope dashboard 95 | '_debugbar*', // Laravel Debugbar 96 | 'health*', // Health check endpoints 97 | 'ping', // Simple ping endpoint 98 | 'status', // Status endpoint 99 | 'metrics', // Metrics endpoint 100 | 'favicon.ico', // Browser favicon requests 101 | 'robots.txt', // SEO robots file 102 | 'sitemap.xml', // SEO sitemap 103 | 'api/health', // API health check 104 | 'api/ping', // API ping 105 | 'admin/health', // Admin health check 106 | 'internal/*', // Internal endpoints 107 | 'monitoring/*', // Monitoring endpoints 108 | '_profiler/*', // Symfony profiler (if used) 109 | '.well-known/*', // Well-known URIs (RFC 8615) 110 | ]))), 111 | ]; 112 | -------------------------------------------------------------------------------- /src/OpenTelemetryServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 26 | __DIR__.'/../config/otel.php' => $this->app->configPath('otel.php'), 27 | ], 'config'); 28 | 29 | // Check if OpenTelemetry is enabled 30 | if (! config('otel.enabled', true)) { 31 | return; 32 | } 33 | 34 | $this->registerCommands(); 35 | $this->registerWatchers(); 36 | $this->registerLifecycleHandlers(); 37 | $this->registerMiddlewares(); 38 | } 39 | 40 | public function register(): void 41 | { 42 | $this->mergeConfigFrom( 43 | __DIR__.'/../config/otel.php', 'otel', 44 | ); 45 | 46 | // Register Tracer 47 | $this->app->singleton(Support\Measure::class, function ($app) { 48 | return new Support\Measure($app); 49 | }); 50 | 51 | $this->app->alias(Support\Measure::class, 'opentelemetry.measure'); 52 | 53 | $this->app->singleton(TracerInterface::class, function () { 54 | return Globals::tracerProvider()->getTracer(config('otel.tracer_name', 'overtrue.laravel-open-telemetry')); 55 | }); 56 | 57 | $this->app->alias(TracerInterface::class, 'opentelemetry.tracer'); 58 | 59 | // Register metric 60 | $this->app->singleton(Support\Metric::class, function ($app) { 61 | return new Support\Metric($app); 62 | }); 63 | $this->app->alias(Support\Metric::class, 'opentelemetry.metric'); 64 | 65 | // register custom meter 66 | $this->app->singleton(MeterInterface::class, function () { 67 | return Globals::meterProvider()->getMeter(config('otel.meter_name', 'overtrue.laravel-open-telemetry')); 68 | }); 69 | 70 | $this->app->alias(MeterInterface::class, 'opentelemetry.meter'); 71 | 72 | Log::debug('[laravel-open-telemetry] Service provider registered successfully'); 73 | } 74 | 75 | /** 76 | * Register lifecycle handlers 77 | */ 78 | protected function registerLifecycleHandlers(): void 79 | { 80 | if (Measure::isOctane()) { 81 | // Octane mode: Listen to Octane events 82 | Event::listen(Events\RequestReceived::class, Handlers\RequestReceivedHandler::class); 83 | Event::listen(Events\RequestTerminated::class, Handlers\RequestTerminatedHandler::class); 84 | Event::listen(Events\RequestHandled::class, Handlers\RequestHandledHandler::class); 85 | Event::listen(Events\WorkerStarting::class, Handlers\WorkerStartingHandler::class); 86 | Event::listen(Events\WorkerErrorOccurred::class, Handlers\WorkerErrorOccurredHandler::class); 87 | Event::listen(Events\TaskReceived::class, Handlers\TaskReceivedHandler::class); 88 | Event::listen(Events\TickReceived::class, Handlers\TickReceivedHandler::class); 89 | } 90 | } 91 | 92 | /** 93 | * Register Watchers 94 | */ 95 | protected function registerWatchers(): void 96 | { 97 | $watchers = config('otel.watchers', []); 98 | 99 | foreach ($watchers as $watcherClass) { 100 | if (class_exists($watcherClass)) { 101 | /** @var \Overtrue\LaravelOpenTelemetry\Watchers\Watcher $watcher */ 102 | $watcher = $this->app->make($watcherClass); 103 | $watcher->register($this->app); 104 | } 105 | } 106 | } 107 | 108 | protected function registerCommands(): void 109 | { 110 | if ($this->app->runningInConsole()) { 111 | $this->commands([ 112 | TestCommand::class, 113 | ]); 114 | } 115 | } 116 | 117 | /** 118 | * Register middlewares 119 | */ 120 | protected function registerMiddlewares(): void 121 | { 122 | $router = $this->app->make('router'); 123 | $kernel = $this->app->make(Kernel::class); 124 | 125 | // Register OpenTelemetry root span middleware 126 | $router->aliasMiddleware('otel', TraceRequest::class); 127 | 128 | $kernel->prependMiddleware(TraceRequest::class); 129 | 130 | // Register Trace ID middleware 131 | if (config('otel.middleware.trace_id.enabled', true)) { 132 | // Register middleware alias 133 | $router->aliasMiddleware('otel.trace_id', AddTraceId::class); 134 | 135 | // Enable TraceId middleware globally by default 136 | if (config('otel.middleware.trace_id.global', true)) { 137 | $kernel->pushMiddleware(AddTraceId::class); 138 | Log::debug('[laravel-open-telemetry] Middleware registered globally for automatic tracing'); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /.cursor-rules: -------------------------------------------------------------------------------- 1 | # Laravel OpenTelemetry Project - Cursor Rules 2 | 3 | 这套 Cursor rule 适用于 Laravel OpenTelemetry 项目的所有编程任务,要求工程师以高级工程师的视角,严格按照流程执行任务,确保代码改动精准、高效,且不会引入问题或不必要的复杂性。 4 | 5 | ## 核心目标 6 | 7 | 确保代码改动精准、高效,且不会引入问题或不必要的复杂性,特别关注 OpenTelemetry 性能监控和 FrankenPHP Worker 模式的特殊需求。 8 | 9 | ## 规则的五个关键步骤 10 | 11 | ### 1. 明确任务范围 12 | - 在写代码前,先分析任务,明确目标 13 | - 制定清晰的计划,列出需要修改的函数、模块或组件,并说明原因 14 | - 特别关注是否涉及 OpenTelemetry 追踪、FrankenPHP Worker 模式或内存管理 15 | - 只有在计划清晰且经过深思熟虑后,才开始写代码 16 | 17 | ### 2. 精准定位代码修改点 18 | - 确定需要修改的具体文件和代码行 19 | - 避免无关文件的改动,若涉及多个文件,需明确说明每个文件的改动理由 20 | - 除非任务明确要求,否则不创建新抽象或重构代码 21 | - 特别注意 Watcher、Hook 和 Support 类的职责边界 22 | 23 | ### 3. 最小化、隔离化的代码改动 24 | - 只编写任务直接所需的代码 25 | - 避免添加不必要的日志、注释、测试、待办事项或错误处理 26 | - 不要进行"顺手"的额外修改,确保新代码不干扰现有功能 27 | - 特别注意不要破坏 OpenTelemetry 的 span 生命周期管理 28 | 29 | ### 4. 严格检查代码 30 | - 检查代码的正确性、是否符合任务范围,以及是否会引发副作用 31 | - 确保代码与现有代码风格一致,防止破坏已有功能 32 | - 评估改动是否会影响下游系统 33 | - 特别关注内存泄漏和性能影响 34 | 35 | ### 5. 清晰交付成果 36 | - 总结改动的具体内容和原因 37 | - 列出所有修改的文件及其具体变更 38 | - 说明任何假设或潜在风险,供他人审查 39 | - 特别说明对 OpenTelemetry 追踪和 FrankenPHP Worker 模式的影响 40 | 41 | ## 核心原则 42 | 43 | - **不即兴发挥**:严格按照任务要求执行,不随意创新 44 | - **不过度设计**:避免复杂化,只做必要的工作 45 | - **不偏离规则**:始终遵循这套流程,确保代码安全、可靠 46 | 47 | ## 项目特定编码规范 48 | 49 | ### PHP 代码规范 50 | - 必须使用 `setAttributes(['user.id' => 123]); 17 | // ... business logic 18 | $span->end(); 19 | 20 | // ======================= Improved Usage Patterns ======================= 21 | 22 | // 1. Use trace() method for automatic span lifecycle management 23 | $user = Measure::trace('user.create', function ($span) { 24 | $span->setAttributes([ 25 | TraceAttributes::ENDUSER_ID => 123, 26 | 'user.action' => 'registration', 27 | ]); 28 | 29 | // Business logic 30 | $user = new User; 31 | $user->save(); 32 | 33 | return $user; 34 | }, ['initial.context' => 'registration']); 35 | 36 | // 2. Semantic HTTP request tracing 37 | Route::middleware('api')->group(function () { 38 | Route::get('/users', function (Request $request) { 39 | // Automatically create HTTP span and set related attributes 40 | $span = Measure::http($request, function ($spanBuilder) { 41 | $spanBuilder->setAttributes([ 42 | 'user.authenticated' => auth()->check(), 43 | 'api.version' => 'v1', 44 | ]); 45 | }); 46 | 47 | $users = User::all(); 48 | $span->end(); 49 | 50 | return response()->json($users); 51 | }); 52 | }); 53 | 54 | // 3. Database operation tracing (using standard semantic conventions) 55 | $users = Measure::trace('user.query', function ($span) { 56 | // Use standard database semantic convention attributes 57 | $span->setAttributes([ 58 | TraceAttributes::DB_SYSTEM => 'mysql', 59 | TraceAttributes::DB_NAMESPACE => 'myapp', 60 | TraceAttributes::DB_COLLECTION_NAME => 'users', 61 | TraceAttributes::DB_OPERATION_NAME => 'SELECT', 62 | ]); 63 | 64 | return User::where('active', true)->get(); 65 | }); 66 | 67 | // 4. HTTP client request tracing 68 | $response = Measure::httpClient('GET', 'https://api.example.com/users', function ($spanBuilder) { 69 | $spanBuilder->setAttributes([ 70 | 'api.client' => 'laravel-http', 71 | 'api.timeout' => 30, 72 | ]); 73 | }); 74 | 75 | // 5. Queue job processing (using standard messaging semantic conventions) 76 | dispatch(function () { 77 | Measure::queue('process', 'EmailJob', function ($spanBuilder) { 78 | $spanBuilder->setAttributes([ 79 | TraceAttributes::MESSAGING_SYSTEM => 'laravel-queue', 80 | TraceAttributes::MESSAGING_DESTINATION_NAME => 'emails', 81 | TraceAttributes::MESSAGING_OPERATION_TYPE => 'PROCESS', 82 | ]); 83 | }); 84 | }); 85 | 86 | // 6. Redis operation tracing 87 | $value = Measure::redis('GET', function ($spanBuilder) { 88 | $spanBuilder->setAttributes([ 89 | TraceAttributes::DB_SYSTEM => 'redis', 90 | TraceAttributes::DB_OPERATION_NAME => 'GET', 91 | 'redis.key' => 'user:123', 92 | ]); 93 | }); 94 | 95 | // 7. Cache operation tracing 96 | $user = Measure::cache('get', 'user:123', function ($spanBuilder) { 97 | $spanBuilder->setAttributes([ 98 | 'cache.store' => 'redis', 99 | 'cache.key' => 'user:123', 100 | ]); 101 | }); 102 | 103 | // 8. Event recording (using standard event semantic conventions) 104 | Measure::event('user.registered', function ($spanBuilder) { 105 | $spanBuilder->setAttributes([ 106 | TraceAttributes::EVENT_NAME => 'user.registered', 107 | TraceAttributes::ENDUSER_ID => 123, 108 | 'event.domain' => 'laravel', 109 | ]); 110 | }); 111 | 112 | // 9. Console command tracing 113 | Artisan::command('users:cleanup', function () { 114 | Measure::command('users:cleanup', function ($spanBuilder) { 115 | $spanBuilder->setAttributes([ 116 | 'console.command' => 'users:cleanup', 117 | 'console.arguments' => '--force', 118 | ]); 119 | }); 120 | }); 121 | 122 | // ======================= Exception Handling and Event Recording ======================= 123 | 124 | try { 125 | $result = Measure::trace('risky.operation', function ($span) { 126 | // Operation that might throw an exception 127 | $span->setAttributes([ 128 | 'operation.type' => 'data_processing', 129 | ]); 130 | 131 | return processData(); 132 | }); 133 | } catch (\Exception $e) { 134 | // Exception will be automatically recorded in the span 135 | Measure::recordException($e); 136 | } 137 | 138 | // Manually add events to current span 139 | Measure::addEvent('checkpoint.reached', [ 140 | 'checkpoint.name' => 'data_validation', 141 | 'checkpoint.status' => 'passed', 142 | ]); 143 | 144 | // ======================= Batch Operation Examples ======================= 145 | 146 | // Batch database operations 147 | Measure::database('BATCH_INSERT', 'users', function ($spanBuilder) { 148 | $spanBuilder->setAttributes([ 149 | TraceAttributes::DB_OPERATION_BATCH_SIZE => 100, 150 | TraceAttributes::DB_SYSTEM => 'mysql', 151 | 'operation.batch' => true, 152 | ]); 153 | }); 154 | 155 | // ======================= Performance Monitoring Examples ======================= 156 | 157 | // Monitor API response time 158 | $users = Measure::trace('api.users.list', function ($span) { 159 | $span->setAttributes([ 160 | TraceAttributes::HTTP_REQUEST_METHOD => 'GET', 161 | 'api.endpoint' => '/users', 162 | 'performance.monitored' => true, 163 | ]); 164 | 165 | $startMemory = memory_get_usage(); 166 | $users = User::with('profile')->paginate(50); 167 | $endMemory = memory_get_usage(); 168 | 169 | $span->setAttributes([ 170 | 'memory.usage_bytes' => $endMemory - $startMemory, 171 | 'result.count' => $users->count(), 172 | ]); 173 | 174 | return $users; 175 | }); 176 | 177 | // ======================= Distributed Tracing Examples ======================= 178 | 179 | // Propagate trace context between microservices 180 | $headers = Measure::propagationHeaders(); 181 | 182 | // Include tracing headers when sending HTTP requests 183 | $response = Http::withHeaders($headers)->get('https://service.example.com/api'); 184 | 185 | // Extract trace context when receiving requests 186 | $context = Measure::extractContextFromPropagationHeaders($request->headers->all()); 187 | -------------------------------------------------------------------------------- /examples/middleware_example.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 55 | 'user.count' => User::count(), 56 | 'request.ip' => request()->ip(), 57 | ]); 58 | 59 | // Execute business logic 60 | $users = User::paginate(15); 61 | 62 | // Add events 63 | $span->addEvent('users.fetched', [ 64 | 'count' => $users->count(), 65 | ]); 66 | 67 | return response()->json($users); 68 | 69 | } catch (\Exception $e) { 70 | // Record exception 71 | $span->recordException($e); 72 | throw $e; 73 | } finally { 74 | // End span 75 | $span->end(); 76 | } 77 | } 78 | 79 | public function show($id) 80 | { 81 | // Use callback approach to create span 82 | return Measure::start('user.show', function ($span) use ($id) { 83 | $span->setAttributes(['user.id' => $id]); 84 | 85 | $user = User::findOrFail($id); 86 | 87 | $span->addEvent('user.found', [ 88 | 'user.email' => $user->email, 89 | ]); 90 | 91 | return response()->json($user); 92 | }); 93 | } 94 | } 95 | 96 | // 4. Using nested tracing in service classes 97 | class UserService 98 | { 99 | public function createUser(array $data) 100 | { 101 | return Measure::start('user.create', function ($span) use ($data) { 102 | $span->setAttributes([ 103 | 'user.email' => $data['email'], 104 | ]); 105 | 106 | // Create nested span 107 | $validationSpan = Measure::start('user.validate'); 108 | $this->validateUserData($data); 109 | $validationSpan->end(); 110 | 111 | // Another nested span 112 | $dbSpan = Measure::start('user.save'); 113 | $user = User::create($data); 114 | $dbSpan->setAttributes(['user.id' => $user->id]); 115 | $dbSpan->end(); 116 | 117 | $span->addEvent('user.created', [ 118 | 'user.id' => $user->id, 119 | ]); 120 | 121 | return $user; 122 | }); 123 | } 124 | 125 | private function validateUserData(array $data) 126 | { 127 | // Validation logic... 128 | } 129 | } 130 | 131 | // 5. Getting current trace information 132 | class ApiController extends Controller 133 | { 134 | public function status() 135 | { 136 | return response()->json([ 137 | 'status' => 'ok', 138 | 'trace_id' => Measure::traceId(), 139 | 'timestamp' => now(), 140 | ]); 141 | } 142 | } 143 | 144 | // 6. Using in middleware 145 | class CustomMiddleware 146 | { 147 | public function handle($request, Closure $next) 148 | { 149 | $span = Measure::start('middleware.custom'); 150 | $span->setAttributes([ 151 | 'http.method' => $request->method(), 152 | 'http.url' => $request->fullUrl(), 153 | ]); 154 | 155 | try { 156 | $response = $next($request); 157 | 158 | $span->setAttributes([ 159 | 'http.status_code' => $response->getStatusCode(), 160 | ]); 161 | 162 | return $response; 163 | } finally { 164 | $span->end(); 165 | } 166 | } 167 | } 168 | 169 | // 7. Production environment configuration example 170 | /* 171 | # Production .env configuration 172 | OTEL_PHP_AUTOLOAD_ENABLED=true 173 | OTEL_SERVICE_NAME=my-production-app 174 | OTEL_SERVICE_VERSION=2.1.0 175 | OTEL_TRACES_EXPORTER=otlp 176 | OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.company.com:4318 177 | OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 178 | OTEL_PROPAGATORS=tracecontext,baggage 179 | 180 | # Sampling configuration 181 | OTEL_TRACES_SAMPLER=traceidratio 182 | OTEL_TRACES_SAMPLER_ARG=0.1 183 | 184 | # Resource attributes 185 | OTEL_RESOURCE_ATTRIBUTES=service.namespace=production,deployment.environment=prod 186 | */ 187 | 188 | // 8. Development environment configuration example 189 | /* 190 | # Development .env configuration 191 | OTEL_PHP_AUTOLOAD_ENABLED=true 192 | OTEL_SERVICE_NAME=my-dev-app 193 | OTEL_TRACES_EXPORTER=console 194 | OTEL_PROPAGATORS=tracecontext,baggage 195 | 196 | # Show all traces during development 197 | OTEL_TRACES_SAMPLER=always_on 198 | */ 199 | 200 | // 9. Automatic HTTP Client Tracing 201 | /* 202 | With the new automatic HTTP client tracing, all HTTP requests made through Laravel's Http facade 203 | are automatically traced with proper context propagation. No manual configuration needed! 204 | 205 | Example: 206 | */ 207 | use Illuminate\Support\Facades\Http; 208 | 209 | class ExternalApiService 210 | { 211 | public function fetchUsers() 212 | { 213 | // This request is automatically traced with context propagation 214 | $response = Http::get('https://api.example.com/users'); 215 | 216 | return $response->json(); 217 | } 218 | 219 | public function createUser(array $userData) 220 | { 221 | // This POST request is also automatically traced 222 | $response = Http::post('https://api.example.com/users', $userData); 223 | 224 | return $response->json(); 225 | } 226 | } 227 | 228 | // 10. Disabling automatic HTTP client propagation (if needed) 229 | /* 230 | If you need to disable automatic HTTP client propagation for specific scenarios: 231 | 232 | In .env: 233 | OTEL_HTTP_CLIENT_PROPAGATION_ENABLED=false 234 | 235 | Or in config/otel.php: 236 | 'http_client' => [ 237 | 'propagation_middleware' => [ 238 | 'enabled' => false, 239 | ], 240 | ], 241 | */ 242 | -------------------------------------------------------------------------------- /src/Support/HttpAttributesHelper.php: -------------------------------------------------------------------------------- 1 | fnmatch($pattern, $request->path())); 23 | } 24 | 25 | /** 26 | * Set HTTP request attributes 27 | */ 28 | public static function setRequestAttributes(SpanInterface $span, Request $request): void 29 | { 30 | $attributes = [ 31 | TraceAttributes::NETWORK_PROTOCOL_VERSION => str_replace('HTTP/', '', $request->getProtocolVersion()), 32 | TraceAttributes::HTTP_REQUEST_METHOD => $request->method(), 33 | TraceAttributes::HTTP_ROUTE => self::getRouteUri($request), 34 | TraceAttributes::URL_FULL => $request->fullUrl(), 35 | TraceAttributes::URL_PATH => $request->path(), 36 | TraceAttributes::URL_QUERY => $request->getQueryString() ?: '', 37 | TraceAttributes::URL_SCHEME => $request->getScheme(), 38 | TraceAttributes::SERVER_ADDRESS => $request->getHost(), 39 | TraceAttributes::CLIENT_ADDRESS => $request->ip(), 40 | TraceAttributes::USER_AGENT_ORIGINAL => $request->userAgent() ?? '', 41 | ]; 42 | 43 | // Add request body size 44 | if ($contentLength = $request->header('Content-Length')) { 45 | $attributes[TraceAttributes::HTTP_REQUEST_BODY_SIZE] = (int) $contentLength; 46 | } elseif ($request->getContent()) { 47 | $attributes[TraceAttributes::HTTP_REQUEST_BODY_SIZE] = strlen($request->getContent()); 48 | } 49 | 50 | // Add client port (if available) 51 | if ($clientPort = $request->header('X-Forwarded-Port') ?: $request->server('REMOTE_PORT')) { 52 | $attributes[TraceAttributes::CLIENT_PORT] = (int) $clientPort; 53 | } 54 | 55 | $span->setAttributes($attributes); 56 | 57 | // Add request headers based on configuration 58 | self::setRequestHeaders($span, $request); 59 | } 60 | 61 | /** 62 | * Set request headers as span attributes based on allowed/sensitive configuration 63 | */ 64 | public static function setRequestHeaders(SpanInterface $span, Request $request): void 65 | { 66 | $allowedHeaders = config('otel.allowed_headers', []); 67 | $sensitiveHeaders = config('otel.sensitive_headers', []); 68 | 69 | if (empty($allowedHeaders)) { 70 | return; 71 | } 72 | 73 | $headers = $request->headers->all(); 74 | 75 | foreach ($headers as $name => $values) { 76 | $headerName = strtolower($name); 77 | $headerValue = is_array($values) ? implode(', ', $values) : (string) $values; 78 | 79 | // Check if header is allowed 80 | if (self::isHeaderAllowed($headerName, $allowedHeaders)) { 81 | $attributeName = 'http.request.header.'.str_replace('-', '_', $headerName); 82 | 83 | // Check if header is sensitive 84 | if (self::isHeaderSensitive($headerName, $sensitiveHeaders)) { 85 | $span->setAttribute($attributeName, '***'); 86 | } else { 87 | $span->setAttribute($attributeName, $headerValue); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Set response headers as span attributes based on allowed/sensitive configuration 95 | */ 96 | public static function setResponseHeaders(SpanInterface $span, Response $response): void 97 | { 98 | $allowedHeaders = config('otel.allowed_headers', []); 99 | $sensitiveHeaders = config('otel.sensitive_headers', []); 100 | 101 | if (empty($allowedHeaders)) { 102 | return; 103 | } 104 | 105 | $headers = $response->headers->all(); 106 | 107 | foreach ($headers as $name => $values) { 108 | $headerName = strtolower($name); 109 | $headerValue = is_array($values) ? implode(', ', $values) : (string) $values; 110 | 111 | // Check if header is allowed 112 | if (self::isHeaderAllowed($headerName, $allowedHeaders)) { 113 | $attributeName = 'http.response.header.'.str_replace('-', '_', $headerName); 114 | 115 | // Check if header is sensitive 116 | if (self::isHeaderSensitive($headerName, $sensitiveHeaders)) { 117 | $span->setAttribute($attributeName, '***'); 118 | } else { 119 | $span->setAttribute($attributeName, $headerValue); 120 | } 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Check if header is allowed based on patterns 127 | */ 128 | private static function isHeaderAllowed(string $headerName, array $allowedHeaders): bool 129 | { 130 | return array_any($allowedHeaders, fn ($pattern) => fnmatch(strtolower($pattern), $headerName)); 131 | 132 | } 133 | 134 | /** 135 | * Check if header is sensitive based on patterns 136 | */ 137 | private static function isHeaderSensitive(string $headerName, array $sensitiveHeaders): bool 138 | { 139 | return array_any($sensitiveHeaders, fn ($pattern) => fnmatch(strtolower($pattern), $headerName)); 140 | } 141 | 142 | /** 143 | * Set HTTP response attributes 144 | */ 145 | public static function setResponseAttributes(SpanInterface $span, Response $response): void 146 | { 147 | $attributes = [ 148 | TraceAttributes::HTTP_RESPONSE_STATUS_CODE => $response->getStatusCode(), 149 | ]; 150 | 151 | // Prefer Content-Length header, otherwise calculate actual content length 152 | if ($contentLength = $response->headers->get('Content-Length')) { 153 | $attributes[TraceAttributes::HTTP_RESPONSE_BODY_SIZE] = (int) $contentLength; 154 | } else { 155 | $attributes[TraceAttributes::HTTP_RESPONSE_BODY_SIZE] = strlen($response->getContent()); 156 | } 157 | 158 | $span->setAttributes($attributes); 159 | 160 | // Add response headers based on configuration 161 | self::setResponseHeaders($span, $response); 162 | } 163 | 164 | /** 165 | * Set span status based on response status code 166 | */ 167 | public static function setSpanStatusFromResponse(SpanInterface $span, Response $response): void 168 | { 169 | if ($response->getStatusCode() >= 400) { 170 | $span->setStatus(StatusCode::STATUS_ERROR, 'HTTP Error'); 171 | } else { 172 | $span->setStatus(StatusCode::STATUS_OK); 173 | } 174 | } 175 | 176 | /** 177 | * Set complete HTTP request and response attributes 178 | */ 179 | public static function setHttpAttributes(SpanInterface $span, Request $request, ?Response $response = null): void 180 | { 181 | self::setRequestAttributes($span, $request); 182 | 183 | if ($response) { 184 | self::setResponseAttributes($span, $response); 185 | self::setSpanStatusFromResponse($span, $response); 186 | } 187 | } 188 | 189 | /** 190 | * Generate span name 191 | */ 192 | public static function generateSpanName(Request $request): string 193 | { 194 | return SpanNameHelper::http($request); 195 | } 196 | 197 | /** 198 | * Get route URI 199 | */ 200 | public static function getRouteUri(Request $request): string 201 | { 202 | try { 203 | /** @var Route $route */ 204 | $route = $request->route(); 205 | if ($route) { 206 | $uri = $route->uri(); 207 | 208 | return $uri === '/' ? '' : $uri; 209 | } 210 | } catch (Throwable $throwable) { 211 | // If route doesn't exist, simply return path 212 | } 213 | 214 | return $request->path(); 215 | } 216 | 217 | /** 218 | * Add Trace ID to response headers 219 | */ 220 | public static function addTraceIdToResponse(SpanInterface $span, Response $response): void 221 | { 222 | $traceId = $span->getContext()->getTraceId(); 223 | if ($traceId) { 224 | $headerName = config('otel.middleware.trace_id.header_name', 'X-Trace-Id'); 225 | $response->headers->set($headerName, $traceId); 226 | } 227 | } 228 | 229 | /** 230 | * Extract trace context carrier from HTTP headers 231 | */ 232 | public static function extractCarrierFromHeaders(Request $request): array 233 | { 234 | $carrier = []; 235 | foreach ($request->headers->all() as $name => $values) { 236 | $carrier[strtolower($name)] = $values[0] ?? ''; 237 | } 238 | 239 | return $carrier; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /examples/measure_semconv_guide.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 19 | TraceAttributes::DB_SYSTEM => 'mysql', // Database system 20 | TraceAttributes::DB_NAMESPACE => 'myapp_production', // Database name 21 | TraceAttributes::DB_COLLECTION_NAME => 'users', // Table name 22 | TraceAttributes::DB_OPERATION_NAME => 'SELECT', // Operation name 23 | TraceAttributes::DB_QUERY_TEXT => 'SELECT * FROM users WHERE active = ?', // Query text 24 | ]); 25 | }); 26 | 27 | // ❌ Incorrect: Using custom attribute names 28 | Measure::database('SELECT', 'users', function ($spanBuilder) { 29 | $spanBuilder->setAttributes([ 30 | 'database.type' => 'mysql', // Should use TraceAttributes::DB_SYSTEM 31 | 'db.name' => 'myapp_production', // Should use TraceAttributes::DB_NAMESPACE 32 | 'table.name' => 'users', // Should use TraceAttributes::DB_COLLECTION_NAME 33 | ]); 34 | }); 35 | 36 | // ======================= HTTP Client Semantic Conventions ======================= 37 | 38 | // ✅ Correct: Using standard HTTP semantic conventions 39 | Measure::httpClient('GET', 'https://api.example.com/users', function ($spanBuilder) { 40 | $spanBuilder->setAttributes([ 41 | TraceAttributes::HTTP_REQUEST_METHOD => 'GET', 42 | TraceAttributes::URL_FULL => 'https://api.example.com/users', 43 | TraceAttributes::URL_SCHEME => 'https', 44 | TraceAttributes::SERVER_ADDRESS => 'api.example.com', 45 | TraceAttributes::SERVER_PORT => 443, 46 | TraceAttributes::USER_AGENT_ORIGINAL => 'Laravel/9.0 Guzzle/7.0', 47 | ]); 48 | }); 49 | 50 | // ❌ Incorrect: Using custom attribute names 51 | Measure::httpClient('GET', 'https://api.example.com/users', function ($spanBuilder) { 52 | $spanBuilder->setAttributes([ 53 | 'http.method' => 'GET', // Should use TraceAttributes::HTTP_REQUEST_METHOD 54 | 'request.url' => 'https://api.example.com/users', // Should use TraceAttributes::URL_FULL 55 | 'host.name' => 'api.example.com', // Should use TraceAttributes::SERVER_ADDRESS 56 | ]); 57 | }); 58 | 59 | // ======================= Messaging Semantic Conventions ======================= 60 | 61 | // ✅ Correct: Using standard messaging semantic conventions 62 | Measure::queue('process', 'SendEmailJob', function ($spanBuilder) { 63 | $spanBuilder->setAttributes([ 64 | TraceAttributes::MESSAGING_SYSTEM => 'laravel-queue', 65 | TraceAttributes::MESSAGING_DESTINATION_NAME => 'emails', 66 | TraceAttributes::MESSAGING_OPERATION_TYPE => 'PROCESS', 67 | TraceAttributes::MESSAGING_MESSAGE_ID => 'msg_12345', 68 | ]); 69 | }); 70 | 71 | // ❌ Incorrect: Using custom attribute names 72 | Measure::queue('process', 'SendEmailJob', function ($spanBuilder) { 73 | $spanBuilder->setAttributes([ 74 | 'queue.system' => 'laravel-queue', // Should use TraceAttributes::MESSAGING_SYSTEM 75 | 'queue.name' => 'emails', // Should use TraceAttributes::MESSAGING_DESTINATION_NAME 76 | 'job.operation' => 'PROCESS', // Should use TraceAttributes::MESSAGING_OPERATION_TYPE 77 | ]); 78 | }); 79 | 80 | // ======================= Event Semantic Conventions ======================= 81 | 82 | // ✅ Correct: Using standard event semantic conventions 83 | Measure::event('user.registered', function ($spanBuilder) { 84 | $spanBuilder->setAttributes([ 85 | TraceAttributes::EVENT_NAME => 'user.registered', 86 | TraceAttributes::ENDUSER_ID => '123', 87 | 'event.domain' => 'laravel', // Custom attribute, as no standard is defined 88 | ]); 89 | }); 90 | 91 | // ======================= Exception Semantic Conventions ======================= 92 | 93 | try { 94 | // Some operation that might fail 95 | throw new \Exception('Something went wrong'); 96 | } catch (\Exception $e) { 97 | // ✅ Correct: Exceptions automatically use standard semantic conventions 98 | Measure::recordException($e); 99 | 100 | // When recording manually, also use standard attributes 101 | Measure::addEvent('exception.occurred', [ 102 | TraceAttributes::EXCEPTION_TYPE => get_class($e), 103 | TraceAttributes::EXCEPTION_MESSAGE => $e->getMessage(), 104 | TraceAttributes::CODE_FILEPATH => $e->getFile(), 105 | TraceAttributes::CODE_LINENO => $e->getLine(), 106 | ]); 107 | } 108 | 109 | // ======================= User Authentication Semantic Conventions ======================= 110 | 111 | // ✅ Correct: Using standard user semantic conventions 112 | Measure::auth('login', function ($spanBuilder) { 113 | $spanBuilder->setAttributes([ 114 | TraceAttributes::ENDUSER_ID => auth()->id(), 115 | TraceAttributes::ENDUSER_ROLE => auth()->user()->role ?? 'user', 116 | // 'auth.method' => 'password', // Custom attribute, as no standard is defined 117 | ]); 118 | }); 119 | 120 | // ======================= Network Semantic Conventions ======================= 121 | 122 | // ✅ Correct: Using standard network semantic conventions 123 | $spanBuilder->setAttributes([ 124 | TraceAttributes::NETWORK_PROTOCOL_NAME => 'http', 125 | TraceAttributes::NETWORK_PROTOCOL_VERSION => '1.1', 126 | TraceAttributes::NETWORK_PEER_ADDRESS => '192.168.1.1', 127 | TraceAttributes::NETWORK_PEER_PORT => 8080, 128 | ]); 129 | 130 | // ======================= Performance Monitoring Semantic Conventions ======================= 131 | 132 | // ✅ Correct: Setting attributes for performance monitoring 133 | Measure::trace('data.processing', function ($span) { 134 | $startTime = microtime(true); 135 | $startMemory = memory_get_usage(); 136 | 137 | // Execute data processing 138 | $result = processLargeDataset(); 139 | 140 | $endTime = microtime(true); 141 | $endMemory = memory_get_usage(); 142 | 143 | $span->setAttributes([ 144 | 'process.runtime.name' => 'php', 145 | 'process.runtime.version' => PHP_VERSION, 146 | 'performance.duration_ms' => ($endTime - $startTime) * 1000, 147 | 'performance.memory_usage_bytes' => $endMemory - $startMemory, 148 | 'data.records_processed' => count($result), 149 | ]); 150 | 151 | return $result; 152 | }); 153 | 154 | // ======================= Cache Operations (No Standard Semantic Conventions Yet) ======================= 155 | 156 | // 📝 Note: Cache operations currently have no standard OpenTelemetry semantic conventions 157 | // We use consistent custom attribute names, awaiting standardization 158 | Measure::cache('get', 'user:123', function ($spanBuilder) { 159 | $spanBuilder->setAttributes([ 160 | 'cache.operation' => 'GET', 161 | 'cache.key' => 'user:123', 162 | 'cache.store' => 'redis', 163 | 'cache.hit' => true, 164 | 'cache.ttl' => 3600, 165 | ]); 166 | }); 167 | 168 | // ======================= Best Practices Summary ======================= 169 | 170 | /** 171 | * 🎯 Semantic Conventions Usage Best Practices: 172 | * 173 | * 1. Prioritize Standard Semantic Conventions 174 | * - Always use predefined constants from OpenTelemetry\SemConv\TraceAttributes 175 | * - Ensure attribute names and values comply with OpenTelemetry specifications 176 | * 177 | * 2. Custom Attribute Naming Standards 178 | * - When no standard semantic conventions exist, use descriptive attribute names 179 | * - Follow the "namespace.attribute" naming pattern 180 | * - Avoid conflicts with existing standard attributes 181 | * 182 | * 3. Attribute Value Standardization 183 | * - Use standard enumerated values (e.g., HTTP method names in uppercase) 184 | * - Maintain consistency and comparability of attribute values 185 | * - Avoid including sensitive information 186 | * 187 | * 4. Backward Compatibility 188 | * - Update promptly when OpenTelemetry releases new semantic conventions 189 | * - Maintain stability of existing custom attributes 190 | * 191 | * 5. Document Custom Attributes 192 | * - Write documentation for project-specific attributes 193 | * - Ensure team members understand attribute meanings and purposes 194 | */ 195 | 196 | // ======================= Common Errors and Corrections ======================= 197 | 198 | // ❌ Incorrect: Using deprecated attribute names 199 | $spanBuilder->setAttributes([ 200 | 'http.method' => 'GET', // Deprecated 201 | 'http.url' => 'https://example.com', // Deprecated 202 | ]); 203 | 204 | // ✅ Correct: Using current standard attributes 205 | $spanBuilder->setAttributes([ 206 | TraceAttributes::HTTP_REQUEST_METHOD => 'GET', // Current standard 207 | TraceAttributes::URL_FULL => 'https://example.com', // Current standard 208 | ]); 209 | -------------------------------------------------------------------------------- /src/Watchers/HttpClientWatcher.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $spans = []; 29 | 30 | public function register(Application $app): void 31 | { 32 | // Register event listeners for span creation 33 | $app['events']->listen(RequestSending::class, [$this, 'recordRequest']); 34 | $app['events']->listen(ConnectionFailed::class, [$this, 'recordConnectionFailed']); 35 | $app['events']->listen(ResponseReceived::class, [$this, 'recordResponse']); 36 | 37 | // Register global HTTP client middleware for automatic context propagation 38 | $this->registerHttpClientMiddleware($app); 39 | } 40 | 41 | public function recordRequest(RequestSending $request): void 42 | { 43 | // Check if request already has GuzzleTraceMiddleware by inspecting headers 44 | // If there are OpenTelemetry propagation headers, it means GuzzleTraceMiddleware is already handling this request 45 | $headers = $request->request->headers(); 46 | if ($this->hasTracingMiddleware($headers)) { 47 | // Skip automatic tracing if manual tracing middleware is already present 48 | return; 49 | } 50 | 51 | $parsedUrl = collect(parse_url($request->request->url()) ?: []); 52 | $processedUrl = $parsedUrl->get('scheme', 'http').'://'.$parsedUrl->get('host').$parsedUrl->get('path', ''); 53 | 54 | if ($parsedUrl->has('query')) { 55 | $processedUrl .= '?'.$parsedUrl->get('query'); 56 | } 57 | 58 | $tracer = Measure::tracer(); 59 | $span = $tracer->spanBuilder(SpanNameHelper::httpClient($request->request->method(), $processedUrl)) 60 | ->setSpanKind(SpanKind::KIND_CLIENT) 61 | ->setParent(Context::getCurrent()) 62 | ->setAttributes([ 63 | TraceAttributes::HTTP_REQUEST_METHOD => $request->request->method(), 64 | TraceAttributes::URL_FULL => $processedUrl, 65 | TraceAttributes::URL_PATH => $parsedUrl['path'] ?? '', 66 | TraceAttributes::URL_SCHEME => $parsedUrl['scheme'] ?? '', 67 | TraceAttributes::SERVER_ADDRESS => $parsedUrl['host'] ?? '', 68 | TraceAttributes::SERVER_PORT => $parsedUrl['port'] ?? '', 69 | ]) 70 | ->startSpan(); 71 | 72 | // Add request headers based on configuration 73 | $this->setRequestHeaders($span, $request->request); 74 | 75 | $this->spans[$this->createRequestComparisonHash($request->request)] = $span; 76 | } 77 | 78 | public function recordConnectionFailed(ConnectionFailed $request): void 79 | { 80 | $requestHash = $this->createRequestComparisonHash($request->request); 81 | 82 | $span = $this->spans[$requestHash] ?? null; 83 | if ($span === null) { 84 | return; 85 | } 86 | 87 | $span->setStatus(StatusCode::STATUS_ERROR, 'Connection failed'); 88 | $span->end(); 89 | 90 | unset($this->spans[$requestHash]); 91 | } 92 | 93 | public function recordResponse(ResponseReceived $request): void 94 | { 95 | $requestHash = $this->createRequestComparisonHash($request->request); 96 | 97 | $span = $this->spans[$requestHash] ?? null; 98 | if ($span === null) { 99 | return; 100 | } 101 | 102 | $span->setAttributes([ 103 | TraceAttributes::HTTP_RESPONSE_STATUS_CODE => $request->response->status(), 104 | TraceAttributes::HTTP_RESPONSE_BODY_SIZE => $request->response->header('Content-Length'), 105 | ]); 106 | 107 | // Add response headers based on configuration 108 | $this->setResponseHeaders($span, $request->response); 109 | 110 | $this->maybeRecordError($span, $request->response); 111 | $span->end(); 112 | 113 | unset($this->spans[$requestHash]); 114 | } 115 | 116 | /** 117 | * Set request headers as span attributes based on allowed/sensitive configuration 118 | */ 119 | private function setRequestHeaders(SpanInterface $span, Request $request): void 120 | { 121 | $allowedHeaders = config('otel.allowed_headers', []); 122 | $sensitiveHeaders = config('otel.sensitive_headers', []); 123 | 124 | if (empty($allowedHeaders)) { 125 | return; 126 | } 127 | 128 | $headers = $request->headers(); 129 | 130 | foreach ($headers as $name => $values) { 131 | $headerName = strtolower($name); 132 | $headerValue = is_array($values) ? implode(', ', $values) : (string) $values; 133 | 134 | // Check if header is allowed 135 | if ($this->isHeaderAllowed($headerName, $allowedHeaders)) { 136 | $attributeName = 'http.request.header.'.str_replace('-', '_', $headerName); 137 | 138 | // Check if header is sensitive 139 | if ($this->isHeaderSensitive($headerName, $sensitiveHeaders)) { 140 | $span->setAttribute($attributeName, '***'); 141 | } else { 142 | $span->setAttribute($attributeName, $headerValue); 143 | } 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Set response headers as span attributes based on allowed/sensitive configuration 150 | */ 151 | private function setResponseHeaders(SpanInterface $span, Response $response): void 152 | { 153 | $allowedHeaders = config('otel.allowed_headers', []); 154 | $sensitiveHeaders = config('otel.sensitive_headers', []); 155 | 156 | if (empty($allowedHeaders)) { 157 | return; 158 | } 159 | 160 | $headers = $response->headers(); 161 | 162 | foreach ($headers as $name => $values) { 163 | $headerName = strtolower($name); 164 | $headerValue = is_array($values) ? implode(', ', $values) : (string) $values; 165 | 166 | // Check if header is allowed 167 | if ($this->isHeaderAllowed($headerName, $allowedHeaders)) { 168 | $attributeName = 'http.response.header.'.str_replace('-', '_', $headerName); 169 | 170 | // Check if header is sensitive 171 | if ($this->isHeaderSensitive($headerName, $sensitiveHeaders)) { 172 | $span->setAttribute($attributeName, '***'); 173 | } else { 174 | $span->setAttribute($attributeName, $headerValue); 175 | } 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * Check if header is allowed based on patterns 182 | */ 183 | private function isHeaderAllowed(string $headerName, array $allowedHeaders): bool 184 | { 185 | return array_any($allowedHeaders, fn ($pattern) => fnmatch(strtolower($pattern), $headerName)); 186 | } 187 | 188 | /** 189 | * Check if header is sensitive based on patterns 190 | */ 191 | private function isHeaderSensitive(string $headerName, array $sensitiveHeaders): bool 192 | { 193 | return array_any($sensitiveHeaders, fn ($pattern) => fnmatch(strtolower($pattern), $headerName)); 194 | } 195 | 196 | private function createRequestComparisonHash(Request $request): string 197 | { 198 | return sha1($request->method().'|'.$request->url().'|'.$request->body()); 199 | } 200 | 201 | private function maybeRecordError(SpanInterface $span, Response $response): void 202 | { 203 | if ($response->successful()) { 204 | return; 205 | } 206 | 207 | // HTTP status code 3xx is not really error 208 | if ($response->redirect()) { 209 | return; 210 | } 211 | 212 | $span->setStatus( 213 | StatusCode::STATUS_ERROR, 214 | HttpResponse::$statusTexts[$response->status()] ?? (string) $response->status() 215 | ); 216 | } 217 | 218 | /** 219 | * Check if the request already has tracing middleware by looking for OpenTelemetry propagation headers 220 | */ 221 | private function hasTracingMiddleware(array $headers): bool 222 | { 223 | // Common OpenTelemetry propagation headers 224 | $tracingHeaders = [ 225 | 'traceparent', 226 | 'tracestate', 227 | 'x-trace-id', 228 | 'x-span-id', 229 | 'b3', 230 | 'x-b3-traceid', 231 | 'x-b3-spanid', 232 | ]; 233 | 234 | return array_any($tracingHeaders, fn ($headerName) => isset($headers[$headerName]) || isset($headers[ucfirst($headerName)]) || isset($headers[strtoupper($headerName)])); 235 | } 236 | 237 | /** 238 | * Register HTTP client middleware for automatic context propagation 239 | */ 240 | protected function registerHttpClientMiddleware(Application $app): void 241 | { 242 | // Check if HTTP client propagation middleware is enabled 243 | if (! config('otel.http_client.propagation_middleware.enabled', true)) { 244 | return; 245 | } 246 | 247 | // Register global request middleware to automatically add propagation headers 248 | Http::globalRequestMiddleware(function ($request) { 249 | $propagationHeaders = Measure::propagationHeaders(); 250 | 251 | foreach ($propagationHeaders as $name => $value) { 252 | $request = $request->withHeader($name, $value); 253 | } 254 | 255 | return $request; 256 | }); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Support/Measure.php: -------------------------------------------------------------------------------- 1 | isOctane()) { 61 | $this->endRootSpan(); 62 | } 63 | } 64 | 65 | // ======================= Root Span Management ======================= 66 | 67 | /** 68 | * Start root span and set it as the current active span. 69 | */ 70 | public function startRootSpan(string $name, array $attributes = [], ?ContextInterface $parentContext = null): SpanInterface 71 | { 72 | $parentContext = $parentContext ?: Context::getRoot(); 73 | $tracer = $this->tracer(); 74 | 75 | $span = $tracer->spanBuilder($name) 76 | ->setSpanKind(SpanKind::KIND_SERVER) 77 | ->setParent($parentContext) 78 | ->setAttributes($attributes) 79 | ->startSpan(); 80 | 81 | // The activate() call returns a ScopeInterface object. We MUST hold on to this object 82 | // and store it in a static property to prevent it from being garbage-collected prematurely. 83 | $scope = $span->storeInContext($parentContext)->activate(); 84 | self::$rootScope = $scope; 85 | 86 | self::$rootSpan = $span; 87 | 88 | return $span; 89 | } 90 | 91 | /** 92 | * Set root span (for Octane mode) 93 | */ 94 | public function setRootSpan(SpanInterface $span, ScopeInterface $scope): void 95 | { 96 | self::$rootSpan = $span; 97 | self::$rootScope = $scope; 98 | } 99 | 100 | /** 101 | * Get root span 102 | */ 103 | public function getRootSpan(): ?SpanInterface 104 | { 105 | return self::$rootSpan; 106 | } 107 | 108 | /** 109 | * End root span 110 | */ 111 | public function endRootSpan(): void 112 | { 113 | if (self::$rootSpan) { 114 | self::$rootSpan->end(); 115 | self::$rootSpan = null; 116 | } 117 | 118 | if (self::$rootScope) { 119 | try { 120 | self::$rootScope->detach(); 121 | } catch (Throwable $e) { 122 | // Scope may have already been detached, ignore errors 123 | } 124 | self::$rootScope = null; 125 | } 126 | } 127 | 128 | // ======================= Core Span API ======================= 129 | 130 | /** 131 | * Create span builder 132 | */ 133 | public function span(string $spanName): SpanBuilder 134 | { 135 | return new SpanBuilder( 136 | $this->tracer()->spanBuilder($spanName) 137 | ); 138 | } 139 | 140 | /** 141 | * Quickly start a span 142 | */ 143 | public function start(string $spanName, ?Closure $callback = null): StartedSpan 144 | { 145 | $span = $this->tracer() 146 | ->spanBuilder($spanName) 147 | ->startSpan(); 148 | 149 | $scope = $span->activate(); 150 | 151 | $startedSpan = new StartedSpan($span, $scope); 152 | 153 | if ($callback) { 154 | $callback($startedSpan); 155 | } 156 | 157 | return $startedSpan; 158 | } 159 | 160 | /** 161 | * Execute a callback with a span 162 | */ 163 | public function trace(string $name, Closure $callback, array $attributes = []): mixed 164 | { 165 | $span = $this->tracer() 166 | ->spanBuilder($name) 167 | ->setAttributes($attributes) 168 | ->startSpan(); 169 | 170 | $scope = $span->storeInContext(Context::getCurrent())->activate(); 171 | 172 | try { 173 | $result = $callback($span); 174 | $span->setStatus(StatusCode::STATUS_OK); 175 | 176 | return $result; 177 | } catch (Throwable $e) { 178 | $span->recordException($e); 179 | $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); 180 | throw $e; 181 | } finally { 182 | $span->end(); 183 | $scope->detach(); 184 | } 185 | } 186 | 187 | /** 188 | * End current active span 189 | */ 190 | public function end(): void 191 | { 192 | $span = Span::getCurrent(); 193 | 194 | if ($span !== Span::getInvalid()) { 195 | $span->end(); 196 | } 197 | } 198 | 199 | // ======================= Event Recording ======================= 200 | 201 | /** 202 | * Add event to current span 203 | */ 204 | public function addEvent(string $name, array $attributes = []): void 205 | { 206 | $this->activeSpan()->addEvent($name, $attributes); 207 | } 208 | 209 | /** 210 | * Record exception 211 | */ 212 | public function recordException(Throwable $exception, array $attributes = []): void 213 | { 214 | $this->activeSpan()->recordException($exception, $attributes); 215 | } 216 | 217 | /** 218 | * Set span status 219 | */ 220 | public function setStatus(string $code, ?string $description = null): void 221 | { 222 | $this->activeSpan()->setStatus($code, $description); 223 | } 224 | 225 | // ======================= Core OpenTelemetry API ======================= 226 | 227 | /** 228 | * Get the tracer instance 229 | */ 230 | public function tracer(): TracerInterface 231 | { 232 | if (! $this->isEnabled()) { 233 | return new NoopTracer; 234 | } 235 | 236 | try { 237 | return $this->app->get(TracerInterface::class); 238 | } catch (Throwable $e) { 239 | Log::error('[laravel-open-telemetry] Tracer not found', [ 240 | 'error' => $e->getMessage(), 241 | 'line' => $e->getLine(), 242 | 'file' => $e->getFile(), 243 | ]); 244 | 245 | return new NoopTracer; 246 | } 247 | } 248 | 249 | /** 250 | * Get current active span 251 | */ 252 | public function activeSpan(): SpanInterface 253 | { 254 | return Span::getCurrent(); 255 | } 256 | 257 | /** 258 | * Get current active scope 259 | */ 260 | public function activeScope(): ?ScopeInterface 261 | { 262 | return Context::storage()->scope(); 263 | } 264 | 265 | /** 266 | * Get current trace ID 267 | */ 268 | public function traceId(): ?string 269 | { 270 | $traceId = $this->activeSpan()->getContext()->getTraceId(); 271 | 272 | return SpanContextValidator::isValidTraceId($traceId) ? $traceId : null; 273 | } 274 | 275 | // ======================= Context Propagation ======================= 276 | 277 | /** 278 | * Get propagator 279 | */ 280 | public function propagator(): TextMapPropagatorInterface 281 | { 282 | return Globals::propagator(); 283 | } 284 | 285 | /** 286 | * Get propagation headers 287 | */ 288 | public function propagationHeaders(?ContextInterface $context = null): array 289 | { 290 | $headers = []; 291 | $this->propagator()->inject($headers, null, $context); 292 | 293 | return $headers; 294 | } 295 | 296 | /** 297 | * Extract context from propagation headers 298 | */ 299 | public function extractContextFromPropagationHeaders(array $headers): ContextInterface 300 | { 301 | return $this->propagator()->extract($headers); 302 | } 303 | 304 | // ======================= Environment Management ======================= 305 | 306 | /** 307 | * Force flush (for Octane mode) 308 | */ 309 | public function flush(): void 310 | { 311 | Globals::tracerProvider()?->forceFlush(); 312 | } 313 | 314 | /** 315 | * Check if in Octane environment 316 | */ 317 | public function isOctane(): bool 318 | { 319 | return isset($_SERVER['LARAVEL_OCTANE']) || isset($_ENV['LARAVEL_OCTANE']); 320 | } 321 | 322 | /** 323 | * Check if current span is recording 324 | */ 325 | public function isRecording(): bool 326 | { 327 | $tracerProvider = Globals::tracerProvider(); 328 | 329 | // Fallback for NoopTracerProvider or other types 330 | return ! ($tracerProvider instanceof NoopTracerProvider); 331 | } 332 | 333 | /** 334 | * Get current tracking status 335 | */ 336 | public function getStatus(): array 337 | { 338 | $tracerProvider = Globals::tracerProvider(); 339 | $isRecording = $this->isRecording(); 340 | $activeSpan = $this->activeSpan(); 341 | $traceId = $activeSpan->getContext()->getTraceId(); 342 | 343 | return [ 344 | 'is_recording' => $isRecording, 345 | 'is_noop' => ! $isRecording, 346 | 'current_trace_id' => $traceId !== '00000000000000000000000000000000' ? $traceId : null, 347 | 'tracer_provider' => [ 348 | 'class' => get_class($tracerProvider), 349 | 'source' => $this->app->bound('opentelemetry.tracer.provider.source') 350 | ? $this->app->get('opentelemetry.tracer.provider.source') 351 | : 'unknown', 352 | ], 353 | ]; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/Console/Commands/TestCommand.php: -------------------------------------------------------------------------------- 1 | info('=== OpenTelemetry Test Command ==='); 28 | $this->info(''); 29 | 30 | if (! config('otel.enabled')) { 31 | $this->error('OpenTelemetry is disabled in config.'); 32 | $this->info('Set OTEL_ENABLED=true in your .env file.'); 33 | 34 | return Command::FAILURE; 35 | } 36 | 37 | // Detect running mode 38 | $hasExtension = extension_loaded('opentelemetry'); 39 | $mode = $hasExtension ? 'Auto-Instrumentation' : 'Manual'; 40 | 41 | $this->info("🔧 Mode: {$mode}"); 42 | $this->info('📦 Extension: '.($hasExtension ? 'Loaded' : 'Not Available')); 43 | $this->info(''); 44 | 45 | // Get detailed status information 46 | $status = Measure::getStatus(); 47 | 48 | $this->info('📊 Current Status:'); 49 | $this->line(' Recording: '.($status['is_recording'] ? 'Yes' : 'No')); 50 | $this->line(" TracerProvider: {$status['tracer_provider']['class']}"); 51 | $this->line(' Source: '.($status['tracer_provider']['source'] ?? 'Unknown').''); 52 | $this->line(" Active Spans: {$status['active_spans_count']}"); 53 | $this->info(''); 54 | 55 | // Create a test span to check what type we get 56 | $rootSpan = Measure::start('Test Span'); 57 | $span = $rootSpan->getSpan(); 58 | $spanClass = get_class($span); 59 | 60 | $this->info("Current Span type: {$spanClass}"); 61 | $this->info(''); 62 | 63 | // Check if we have a recording span 64 | if ($span instanceof NonRecordingSpan || ! Measure::isRecording()) { 65 | $this->warn('⚠️ OpenTelemetry is using NonRecordingSpan!'); 66 | $this->info(''); 67 | $this->info('This means OpenTelemetry SDK is not properly configured.'); 68 | $this->info(''); 69 | $this->info('📋 Required environment variables for OpenTelemetry:'); 70 | $this->info(''); 71 | $this->line(' OTEL_PHP_AUTOLOAD_ENABLED=true # Enable PHP auto-instrumentation'); 72 | $this->line(' OTEL_SERVICE_NAME=my-app # Your service name'); 73 | $this->line(' OTEL_TRACES_EXPORTER=console # Export to console (for testing)'); 74 | $this->line(' # OR'); 75 | $this->line(' OTEL_TRACES_EXPORTER=otlp # Export to OTLP collector'); 76 | $this->line(' OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318'); 77 | $this->info(''); 78 | $this->info('💡 For testing with this package (manual instrumentation), add this to your .env file:'); 79 | $this->info(''); 80 | $this->line(' OTEL_ENABLED=true'); 81 | $this->line(' OTEL_SDK_AUTO_INITIALIZE=true'); 82 | $this->line(' OTEL_SERVICE_NAME=laravel-otel-test'); 83 | $this->line(' OTEL_TRACES_EXPORTER=console'); 84 | $this->info(''); 85 | $this->warn('After adding these variables, restart your application and try again.'); 86 | 87 | // Still continue with the test to show what would happen 88 | $this->info(''); 89 | $this->info('Continuing with test (spans will not be recorded)...'); 90 | } else { 91 | $this->info('✅ OpenTelemetry is properly configured!'); 92 | $this->info('Creating test spans...'); 93 | } 94 | 95 | $this->info(''); 96 | 97 | $rootSpan->setAttribute('test.attribute', 'test_value'); 98 | $timestamp = time(); 99 | $rootSpan->setAttribute('timestamp', $timestamp); 100 | 101 | // Simulate delay 102 | $this->info('Creating child span...'); 103 | sleep(1); 104 | 105 | // Add child span 106 | $childSpan = Measure::start('Child Operation'); 107 | $childSpan->setAttribute('child.attribute', 'child_value'); 108 | 109 | sleep(1); 110 | 111 | // End child span 112 | Measure::end(); 113 | $this->info('Child span completed.'); 114 | 115 | // Record event 116 | $rootSpan->addEvent('Test Event', [ 117 | 'detail' => 'This is a test event', 118 | 'timestamp' => $timestamp, 119 | ]); 120 | 121 | // Set status 122 | $span->setStatus(StatusCode::STATUS_OK); 123 | 124 | // Get trace ID before ending the root span 125 | $traceId = $span->getContext()->getTraceId(); 126 | 127 | // End root span 128 | Measure::end(); 129 | 130 | // Output result 131 | $this->info(''); 132 | $this->info('✅ Test completed!'); 133 | $this->info("📊 Trace ID: {$traceId}"); 134 | 135 | if ($traceId === '00000000000000000000000000000000') { 136 | $this->warn('⚠️ Trace ID is all zeros - this indicates NonRecordingSpan'); 137 | } 138 | 139 | // Display summary table 140 | $this->info(''); 141 | $this->table( 142 | ['Span Name', 'Status', 'Attributes'], 143 | [ 144 | ['Test Span', 'OK', "test.attribute=test_value, timestamp={$timestamp}"], 145 | ['Child Operation', 'OK', 'child.attribute=child_value'], 146 | ] 147 | ); 148 | 149 | // Check enhancement status 150 | $this->checkEnhancementStatus(); 151 | 152 | // Display final status 153 | $finalStatus = Measure::getStatus(); 154 | $this->info(''); 155 | $this->info('📈 Final Status:'); 156 | $this->line(' Recording: '.($finalStatus['is_recording'] ? 'Yes' : 'No')); 157 | $this->line(" Active Spans: {$finalStatus['active_spans_count']}"); 158 | $this->line(' Current Trace ID: '.($finalStatus['current_trace_id'] ? "{$finalStatus['current_trace_id']}" : 'None')); 159 | 160 | $this->info(''); 161 | $this->info('🔍 Environment Check:'); 162 | $envVars = [ 163 | 'OTEL_ENABLED' => config('otel.enabled') ? 'true' : 'false', 164 | 'OTEL_SDK_AUTO_INITIALIZE' => config('otel.sdk.auto_initialize') ? 'true' : 'false', 165 | 'OTEL_SERVICE_NAME' => config('otel.sdk.service_name', 'not set'), 166 | 'OTEL_TRACES_EXPORTER' => config('otel.exporters.traces', 'not set'), 167 | ]; 168 | 169 | foreach ($envVars as $key => $value) { 170 | $status = $value === 'not set' ? 'not set' : "{$value}"; 171 | $this->line(" {$key}: {$status}"); 172 | } 173 | 174 | return Command::SUCCESS; 175 | } 176 | 177 | /** 178 | * Check enhancement status 179 | */ 180 | private function checkEnhancementStatus(): void 181 | { 182 | $this->info(''); 183 | $this->info('🚀 Enhancement Status:'); 184 | 185 | // Check official package 186 | $hasOfficialPackage = class_exists('OpenTelemetry\\Contrib\\Instrumentation\\Laravel\\LaravelInstrumentation'); 187 | $this->line(' Official Laravel Package: '.($hasOfficialPackage ? 'Installed' : 'Not Installed')); 188 | 189 | // Check our enhancement features 190 | $hasEnhancement = class_exists('Overtrue\\LaravelOpenTelemetry\\Support\\AutoInstrumentation\\LaravelInstrumentation'); 191 | $this->line(' Enhancement Classes: '.($hasEnhancement ? 'Available' : 'Not Available')); 192 | 193 | // Check if _register.php is in autoload 194 | $autoloadFilePath = base_path('vendor/composer/autoload_files.php'); 195 | $hasRegisterFile = false; 196 | if (file_exists($autoloadFilePath)) { 197 | $composerAutoload = file_get_contents($autoloadFilePath); 198 | $hasRegisterFile = strpos($composerAutoload, '_register.php') !== false; 199 | } 200 | $this->line(' Auto Register File: '.($hasRegisterFile ? 'Loaded' : 'Not Detected')); 201 | 202 | // Check Guzzle macro 203 | $hasGuzzleMacro = \Illuminate\Http\Client\PendingRequest::hasMacro('withTrace'); 204 | $this->line(' Guzzle Trace Macro: '.($hasGuzzleMacro ? 'Registered' : 'Not Registered')); 205 | 206 | // Check Watcher status 207 | $this->info(''); 208 | $this->info('👀 Watchers Status:'); 209 | $watchers = config('otel.watchers', []); 210 | if (empty($watchers)) { 211 | $this->line(' No watchers configured'); 212 | } else { 213 | foreach ($watchers as $watcherClass) { 214 | $className = class_basename($watcherClass); 215 | $exists = class_exists($watcherClass); 216 | $status = $exists ? 'Enabled' : 'Class not found'; 217 | $this->line(" {$className}: {$status}"); 218 | } 219 | } 220 | 221 | // Check if Watcher classes exist 222 | $watcherClasses = [ 223 | 'ExceptionWatcher' => 'Overtrue\\LaravelOpenTelemetry\\Watchers\\ExceptionWatcher', 224 | 'AuthenticateWatcher' => 'Overtrue\\LaravelOpenTelemetry\\Watchers\\AuthenticateWatcher', 225 | 'EventWatcher' => 'Overtrue\\LaravelOpenTelemetry\\Watchers\\EventWatcher', 226 | 'QueueWatcher' => 'Overtrue\\LaravelOpenTelemetry\\Watchers\\QueueWatcher', 227 | 'RedisWatcher' => 'Overtrue\\LaravelOpenTelemetry\\Watchers\\RedisWatcher', 228 | ]; 229 | 230 | $this->info(''); 231 | $this->info('🔍 Watcher Classes Status:'); 232 | foreach ($watcherClasses as $name => $class) { 233 | $exists = class_exists($class); 234 | $this->line(" {$name}: ".($exists ? 'Available' : 'Not Available')); 235 | } 236 | 237 | if (! $hasOfficialPackage) { 238 | $this->warn('⚠️ Recommend installing the official opentelemetry-auto-laravel package for full functionality'); 239 | } 240 | 241 | if (! $hasEnhancement) { 242 | $this->error('❌ Enhancement classes not available, please check package installation'); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel OpenTelemetry 2 | 3 | > **⚠️ Deprecated**: This package is no longer maintained. Please consider using [keepsuit/laravel-opentelemetry](https://github.com/keepsuit/laravel-opentelemetry) instead. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/overtrue/laravel-open-telemetry.svg?style=flat-square)](https://packagist.org/packages/overtrue/laravel-open-telemetry) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/overtrue/laravel-open-telemetry.svg?style=flat-square)](https://packagist.org/packages/overtrue/laravel-open-telemetry) 7 | 8 | This package provides a simple way to add OpenTelemetry to your Laravel application. 9 | 10 | ## Features 11 | 12 | - ✅ **Zero Configuration**: Works out of the box with sensible defaults. 13 | - ✅ **Laravel Native**: Deep integration with Laravel's lifecycle and events. 14 | - ✅ **Octane & FPM Support**: Full compatibility with Laravel Octane and traditional FPM setups. 15 | - ✅ **Powerful `Measure` Facade**: Provides an elegant API for manual, semantic tracing. 16 | - ✅ **Automatic Tracing**: Built-in watchers for cache, database, HTTP clients, queues, and more. 17 | - ✅ **Flexible Configuration**: Control traced paths, headers, and watchers to fit your needs. 18 | - ✅ **Standards Compliant**: Adheres to OpenTelemetry Semantic Conventions. 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require overtrue/laravel-open-telemetry 26 | ``` 27 | 28 | ## Configuration 29 | 30 | > **Important Note for Octane Users** 31 | > 32 | > When using Laravel Octane, it is **highly recommended** to set `OTEL_*` environment variables at the machine or process level (e.g., in your Dockerfile, `docker-compose.yml`, or Supervisor configuration) rather than relying solely on the `.env` file. 33 | > 34 | > This is because some OpenTelemetry components, especially those enabled by `OTEL_PHP_AUTOLOAD_ENABLED`, are initialized before the Laravel application fully boots and reads the `.env` file. Setting them as system-level environment variables ensures they are available to the PHP process from the very beginning. 35 | 36 | This package uses the standard OpenTelemetry environment variables for configuration. Add these to your `.env` file for basic setup: 37 | 38 | ### Basic Configuration 39 | 40 | ```env 41 | # Enable OpenTelemetry PHP SDK auto-loading 42 | OTEL_PHP_AUTOLOAD_ENABLED=true 43 | 44 | # Service identification 45 | OTEL_SERVICE_NAME=my-laravel-app 46 | OTEL_SERVICE_VERSION=1.0.0 47 | 48 | # Exporter configuration (console for dev, otlp for prod) 49 | OTEL_TRACES_EXPORTER=console 50 | OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 51 | OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 52 | 53 | # Context propagation 54 | OTEL_PROPAGATORS=tracecontext,baggage 55 | ``` 56 | 57 | ### Package Configuration 58 | 59 | For package-specific settings, publish the configuration file: 60 | 61 | ```bash 62 | php artisan vendor:publish --provider="Overtrue\LaravelOpenTelemetry\OpenTelemetryServiceProvider" --tag=config 63 | ``` 64 | 65 | This will create a `config/otel.php` file. Here are the key options: 66 | 67 | #### Enabling/Disabling Tracing 68 | 69 | You can completely enable or disable tracing for the entire application. This is useful for performance tuning or disabling tracing in certain environments. 70 | 71 | ```php 72 | // config/otel.php 73 | 'enabled' => env('OTEL_ENABLED', true), 74 | ``` 75 | Set `OTEL_ENABLED=false` in your `.env` file to disable all tracing. 76 | 77 | **Log Format**: All OpenTelemetry logs are prefixed with `[laravel-open-telemetry]` for easy identification and filtering. 78 | 79 | #### Filtering Requests and Headers 80 | 81 | You can control which requests are traced and which headers are recorded to enhance performance and protect sensitive data. All patterns support wildcards (`*`) and are case-insensitive. 82 | 83 | - **`ignore_paths`**: A list of request paths to exclude from tracing. Useful for health checks, metrics endpoints, etc. 84 | ```php 85 | 'ignore_paths' => ['health*', 'telescope*', 'horizon*'], 86 | ``` 87 | - **`allowed_headers`**: A list of HTTP header patterns to include in spans. If empty, no headers are recorded. 88 | ```php 89 | 'allowed_headers' => ['x-request-id', 'user-agent', 'authorization'], 90 | ``` 91 | - **`sensitive_headers`**: A list of header patterns whose values will be masked (replaced with `***`). 92 | ```php 93 | 'sensitive_headers' => ['authorization', 'cookie', 'x-api-key', '*-token'], 94 | ``` 95 | 96 | #### Watchers 97 | 98 | You can enable or disable specific watchers to trace different parts of your application. 99 | 100 | ```php 101 | // config/otel.php 102 | 'watchers' => [ 103 | \Overtrue\LaravelOpenTelemetry\Watchers\CacheWatcher::class => env('OTEL_CACHE_WATCHER_ENABLED', true), 104 | \Overtrue\LaravelOpenTelemetry\Watchers\QueryWatcher::class => env('OTEL_QUERY_WATCHER_ENABLED', true), 105 | // ... 106 | ], 107 | ``` 108 | 109 | ## Usage 110 | 111 | The package is designed to work with minimal manual intervention, but it also provides a powerful `Measure` facade for creating custom spans. 112 | 113 | ### Automatic Tracing 114 | 115 | With the default configuration, the package automatically traces: 116 | - Incoming HTTP requests. 117 | - Database queries (`QueryWatcher`). 118 | - Cache operations (`CacheWatcher`). 119 | - Outgoing HTTP client requests (`HttpClientWatcher`). 120 | - Thrown exceptions (`ExceptionWatcher`). 121 | - Queue jobs (`QueueWatcher`). 122 | - ...and more, depending on the enabled [watchers](#watchers). 123 | 124 | ### Creating Custom Spans with `Measure::trace()` 125 | 126 | For tracing specific blocks of code, the `Measure::trace()` method is the recommended approach. It automatically handles span creation, activation, exception recording, and completion. 127 | 128 | ```php 129 | use Overtrue\LaravelOpenTelemetry\Facades\Measure; 130 | 131 | Measure::trace('process-user-data', function ($span) use ($user) { 132 | // Add attributes to the span 133 | $span->setAttribute('user.id', $user->id); 134 | 135 | // Your business logic here 136 | $this->process($user); 137 | 138 | // Add an event to mark a point in time within the span 139 | $span->addEvent('User processing finished'); 140 | }); 141 | ``` 142 | 143 | The `trace` method will: 144 | - Start a new span. 145 | - Execute the callback. 146 | - Automatically record and re-throw any exceptions that occur within the callback. 147 | - End the span when the callback completes. 148 | 149 | ### Advanced Span Creation with SpanBuilder 150 | 151 | For more control over span lifecycle, you can use the `SpanBuilder` directly through `Measure::span()`. The SpanBuilder provides several methods for different use cases: 152 | 153 | #### Basic Span Creation (Recommended for most cases) 154 | 155 | ```php 156 | // Create a span without activating its scope (safer for async operations) 157 | $span = Measure::span('my-operation') 158 | ->setAttribute('operation.type', 'data-processing') 159 | ->setSpanKind(SpanKind::KIND_INTERNAL) 160 | ->start(); // Returns SpanInterface 161 | 162 | // Your business logic here 163 | $result = $this->processData(); 164 | 165 | // Remember to end the span manually 166 | $span->end(); 167 | ``` 168 | 169 | #### Span with Activated Scope 170 | 171 | ```php 172 | // Create a span and activate its scope (for nested operations) 173 | $startedSpan = Measure::span('parent-operation') 174 | ->setAttribute('operation.type', 'user-workflow') 175 | ->setSpanKind(SpanKind::KIND_INTERNAL) 176 | ->startAndActivate(); // Returns StartedSpan 177 | 178 | // Any spans created within this block will be children of this span 179 | $childSpan = Measure::span('child-operation')->start(); 180 | $childSpan->end(); 181 | 182 | // The StartedSpan automatically manages scope cleanup 183 | $startedSpan->end(); // Ends span and detaches scope 184 | ``` 185 | 186 | #### Span with Context (For Manual Propagation) 187 | 188 | ```php 189 | // Create a span and get both span and context for manual management 190 | [$span, $context] = Measure::span('async-operation') 191 | ->setAttribute('operation.async', true) 192 | ->startWithContext(); // Returns [SpanInterface, ContextInterface] 193 | 194 | // Use context for propagation (e.g., in HTTP headers) 195 | $headers = Measure::propagationHeaders($context); 196 | 197 | // Your async operation here 198 | $span->end(); 199 | ``` 200 | 201 | ### Using Semantic Spans 202 | 203 | To promote standardization, the package provides semantic helper methods that create spans with attributes conforming to OpenTelemetry's [Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/). 204 | 205 | #### Database Spans 206 | ```php 207 | // Manually trace a block of database operations 208 | $user = Measure::database('SELECT', 'users'); // Quick shortcut for database operations 209 | // Or use the general trace method for more complex operations 210 | $user = Measure::trace('repository:find-user', function ($span) use ($userId) { 211 | $span->setAttribute('db.statement', "SELECT * FROM users WHERE id = ?"); 212 | $span->setAttribute('db.table', 'users'); 213 | return User::find($userId); 214 | }); 215 | ``` 216 | *Note: If `QueryWatcher` is enabled, individual queries are already traced. This is useful for tracing a larger transaction or a specific business operation involving multiple queries.* 217 | 218 | #### HTTP Client Spans 219 | ```php 220 | // Quick shortcut for HTTP client requests 221 | $response = Measure::httpClient('POST', 'https://api.example.com/users'); 222 | // Or use the general trace method for more control 223 | $response = Measure::trace('api-call', function ($span) { 224 | $span->setAttribute('http.method', 'POST'); 225 | $span->setAttribute('http.url', 'https://api.example.com/users'); 226 | return Http::post('https://api.example.com/users', $data); 227 | }); 228 | ``` 229 | 230 | #### Custom Spans 231 | ```php 232 | // For any custom operation, use the general trace method 233 | $result = Measure::trace('process-payment', function ($span) use ($payment) { 234 | $span->setAttribute('payment.amount', $payment->amount); 235 | $span->setAttribute('payment.currency', $payment->currency); 236 | 237 | // Your business logic here 238 | return $this->processPayment($payment); 239 | }); 240 | ``` 241 | 242 | ### Retrieving the Current Span 243 | 244 | You can access the currently active span anywhere in your code. 245 | 246 | ```php 247 | use Overtrue\LaravelOpenTelemetry\Facades\Measure; 248 | 249 | $currentSpan = Measure::activeSpan(); 250 | $currentSpan->setAttribute('custom.attribute', 'some_value'); 251 | ``` 252 | 253 | ### Watchers 254 | 255 | The package includes several watchers that automatically create spans for common Laravel operations. You can enable or disable them in `config/otel.php`. 256 | 257 | - **`CacheWatcher`**: Traces cache hits, misses, writes, and forgets. 258 | - **`QueryWatcher`**: Traces the execution of every database query. 259 | - **`HttpClientWatcher`**: Traces all outgoing HTTP requests made with Laravel's `Http` facade. 260 | - **`ExceptionWatcher`**: Traces all exceptions thrown in your application. 261 | - **`QueueWatcher`**: Traces jobs being dispatched, processed, and failing. 262 | - **`RedisWatcher`**: Traces Redis commands. 263 | - **`AuthenticateWatcher`**: Traces authentication events like login, logout, and failed attempts. 264 | 265 | 266 | ### Trace ID Injection Middleware 267 | 268 | The package includes middleware to add a `X-Trace-Id` header to your HTTP responses, which is useful for debugging. 269 | 270 | You can apply it to specific routes: 271 | ```php 272 | // In your routes/web.php or routes/api.php 273 | Route::middleware('otel.traceid')->group(function () { 274 | Route::get('/api/users', [UserController::class, 'index']); 275 | }); 276 | ``` 277 | 278 | Or apply it globally in `app/Http/Kernel.php`: 279 | ```php 280 | // app/Http/Kernel.php 281 | 282 | // In the $middlewareGroups property for 'web' or 'api' 283 | protected $middlewareGroups = [ 284 | 'web' => [ 285 | // ... 286 | \Overtrue\LaravelOpenTelemetry\Http\Middleware\AddTraceId::class, 287 | ], 288 | // ... 289 | ]; 290 | ``` 291 | 292 | ## Environment Variables Reference 293 | 294 | ### Core OpenTelemetry Variables 295 | 296 | | Variable | Description | Default | Example | 297 | |----------|-------------|---------|---------| 298 | | `OTEL_PHP_AUTOLOAD_ENABLED` | Enable PHP SDK auto-loading | `false` | `true` | 299 | | `OTEL_SERVICE_NAME` | Service name | `unknown_service` | `my-laravel-app` | 300 | | `OTEL_SERVICE_VERSION` | Service version | `null` | `1.0.0` | 301 | | `OTEL_TRACES_EXPORTER` | Trace exporter type | `otlp` | `console`, `otlp` | 302 | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | `http://localhost:4318` | `https://api.honeycomb.io` | 303 | | `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol | `http/protobuf` | `http/protobuf`, `grpc` | 304 | | `OTEL_PROPAGATORS` | Context propagators | `tracecontext,baggage` | `tracecontext,baggage,b3` | 305 | | `OTEL_TRACES_SAMPLER` | Sampling strategy | `parentbased_always_on` | `always_on`, `traceidratio` | 306 | | `OTEL_TRACES_SAMPLER_ARG` | Sampler argument | `null` | `0.1` | 307 | | `OTEL_RESOURCE_ATTRIBUTES` | Resource attributes | `null` | `key1=value1,key2=value2` | 308 | 309 | ## Testing 310 | 311 | ```bash 312 | composer test 313 | ``` 314 | 315 | ## Changelog 316 | 317 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 318 | 319 | ## Contributing 320 | 321 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 322 | 323 | ## Security Vulnerabilities 324 | 325 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 326 | 327 | ## Credits 328 | 329 | - [overtrue](https://github.com/overtrue) 330 | - [All Contributors](../../contributors) 331 | 332 | ## License 333 | 334 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 335 | --------------------------------------------------------------------------------