├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
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 | [](https://packagist.org/packages/overtrue/laravel-open-telemetry)
6 | [](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 |
--------------------------------------------------------------------------------