├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── opentelemetry.php ├── phpstan.neon └── src ├── Facades ├── Logger.php └── Tracer.php ├── Instrumentation ├── CacheInstrumentation.php ├── EventInstrumentation.php ├── HandlesHttpHeaders.php ├── HttpClientInstrumentation.php ├── HttpServerInstrumentation.php ├── Instrumentation.php ├── QueryInstrumentation.php ├── QueueInstrumentation.php ├── RedisInstrumentation.php └── SpanTimeAdapter.php ├── InstrumentationServiceProvider.php ├── LaravelOpenTelemetryServiceProvider.php ├── Logger.php ├── Support ├── CarbonClock.php ├── HttpClient │ └── GuzzleTraceMiddleware.php ├── HttpServer │ └── TraceRequestMiddleware.php ├── OpenTelemetryMonologHandler.php ├── PropagatorBuilder.php ├── SamplerBuilder.php └── SpanBuilder.php └── Tracer.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-opentelemetry` will be documented in this file. 4 | 5 | ## v1.4.0 - 2025-05-23 6 | 7 | ### What's Changed 8 | 9 | * Skip recording of internal spans when trace is not started by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/33 10 | 11 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/1.3.0...1.4.0 12 | 13 | ## v1.3.0 - 2025-03-22 14 | 15 | ### What's Changed 16 | 17 | * allow to add custom headers to otlp exporter by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/32 18 | 19 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/1.2.0...1.3.0 20 | 21 | ## v1.2.0 - 2025-02-23 22 | 23 | ### What's Changed 24 | 25 | * replaced deprecated `db.system` with `db.system.name` 26 | * Support laravel 12 27 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/keepsuit/laravel-opentelemetry/pull/30 28 | 29 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/1.1.0...1.2.0 30 | 31 | ## v1.1.0 - 2025-01-16 32 | 33 | ### What's Changed 34 | 35 | * Add support for timeout and maxRetries by @ilicmilan in https://github.com/keepsuit/laravel-opentelemetry/pull/28 36 | 37 | ### New Contributors 38 | 39 | * @ilicmilan made their first contribution in https://github.com/keepsuit/laravel-opentelemetry/pull/28 40 | 41 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/1.0.0...1.1.0 42 | 43 | ## v1.0.0 - 2024-10-06 44 | 45 | ### Breaking changes 46 | 47 | This release contains a lot of breaking changes 48 | 49 | * Compare your published config and update it accordingly. 50 | * ENV variables prefix is changed from `OT` to `OTEL` and some env variables are changed. 51 | * Tracer has been completely refactored, manual traces must been updated with the new methods. 52 | 53 | `Tracer::start`, `Tracer::measure` and `Tracer::measureAsync` has been removed and replaced with: 54 | 55 | ```php 56 | Tracer::newSpan('name')->start(); // same as old start 57 | Tracer::newSpan('name')->measure(callback); // same as old measure 58 | Tracer::newSpan('name')->setSpanKind(SpanKind::KIND_PRODUCER)->measure(callback); // same as old measureAsync 59 | 60 | 61 | 62 | 63 | 64 | ``` 65 | `Tracer::recordExceptionToSpan` has been removed and exception should be recorded directly to span: `$span->recordException($exception)` 66 | 67 | `Tracer::setRootSpan($span)` has been removed, it was only used to share traceId with log context. This has been replaced with `Tracer::updateLogContext()` 68 | 69 | ##### Logging 70 | 71 | This release introduce a custom log channel for laravel `otlp` that allows to collect laravel logs with OpenTelemetry. 72 | This is the injected `otlp` channel: 73 | 74 | ```php 75 | // config/logging.php 76 | 'channels' => [ 77 | // injected channel config, you can override it adding an `otlp` channel in your config 78 | 'otlp' => [ 79 | 'driver' => 'monolog', 80 | 'handler' => \Keepsuit\LaravelOpenTelemetry\Support\OpenTelemetryMonologHandler::class, 81 | 'level' => 'debug', 82 | ] 83 | ] 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | ### What's Changed 91 | 92 | * Refactoring tracer api by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/12 93 | * Track http headers by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/13 94 | * Improved http client tests by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/15 95 | * Drop laravel 9 by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/16 96 | * Auto queue instrumentation by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/14 97 | * Follow OTEL env variables specifications by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/17 98 | * Add support for logs by @cappuc in https://github.com/keepsuit/laravel-opentelemetry/pull/20 99 | 100 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/0.4.0...1.0.0 101 | 102 | ## v0.4.1 - 2024-10-06 103 | 104 | ### What's changed 105 | 106 | * Fix #27 107 | 108 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/0.4.0...0.4.1 109 | 110 | ## v0.4.0 - 2024-01-05 111 | 112 | ### What's Changed 113 | 114 | * Replaced deprecated trace attributes 115 | * Increased phpstan level to 7 116 | * Required stable versions of opentelemetry 117 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/keepsuit/laravel-opentelemetry/pull/6 118 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/keepsuit/laravel-opentelemetry/pull/7 119 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/keepsuit/laravel-opentelemetry/pull/8 120 | 121 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/0.3.2...0.4.0 122 | 123 | ## v0.3.1 - 2023-07-10 124 | 125 | ### What's Changed 126 | 127 | - Support for latest open-temetry sdk (which contains some namespace changes) 128 | 129 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/0.3.0...0.3.1 130 | 131 | ## v0.3.0 - 2023-04-13 132 | 133 | ### What's Changed 134 | 135 | - Changed some span names and attributes values to better respect specs 136 | - Added `CacheInstrumentation` which records cache events 137 | - Added `EventInstrumentation` which records dispatched events 138 | 139 | **Full Changelog**: https://github.com/keepsuit/laravel-opentelemetry/compare/0.2.0...0.3.0 140 | 141 | ## v0.2.0 - 2023-03-02 142 | 143 | - Removed deprecated `open-telemetry/sdk-contrib` and use `open-telemetry/exporter-*` packages 144 | 145 | ## v0.1.0 - 2023-01-20 146 | 147 | - First release 148 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) keepsuit 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry integration for laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/keepsuit/laravel-opentelemetry.svg?style=flat-square)](https://packagist.org/packages/keepsuit/laravel-opentelemetry) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/keepsuit/laravel-opentelemetry/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/keepsuit/laravel-opentelemetry/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/keepsuit/laravel-opentelemetry/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/keepsuit/laravel-opentelemetry/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/keepsuit/laravel-opentelemetry.svg?style=flat-square)](https://packagist.org/packages/keepsuit/laravel-opentelemetry) 7 | 8 | _OpenTelemetry is a collection of tools, APIs, and SDKs. Use it to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) to help you analyze your software’s performance and behavior._ 9 | 10 | This package allow to integrate OpenTelemetry in a Laravel application. 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | ```bash 17 | composer require keepsuit/laravel-opentelemetry 18 | ``` 19 | 20 | You can publish the config file with: 21 | 22 | ```bash 23 | php artisan vendor:publish --provider="Keepsuit\LaravelOpenTelemetry\LaravelOpenTelemetryServiceProvider" --tag="opentelemetry-config" 24 | ``` 25 | 26 | This is the contents of the published config file: 27 | 28 | ```php 29 | env('OTEL_SERVICE_NAME', \Illuminate\Support\Str::slug(env('APP_NAME', 'laravel-app'))), 38 | 39 | /** 40 | * Comma separated list of propagators to use. 41 | * Supports any otel propagator, for example: "tracecontext", "baggage", "b3", "b3multi", "none" 42 | */ 43 | 'propagators' => env('OTEL_PROPAGATORS', 'tracecontext'), 44 | 45 | /** 46 | * OpenTelemetry Traces configuration 47 | */ 48 | 'traces' => [ 49 | /** 50 | * Traces exporter 51 | * This should be the key of one of the exporters defined in the exporters section 52 | */ 53 | 'exporter' => env('OTEL_TRACES_EXPORTER', 'otlp'), 54 | 55 | /** 56 | * Traces sampler 57 | */ 58 | 'sampler' => [ 59 | /** 60 | * Wraps the sampler in a parent based sampler 61 | */ 62 | 'parent' => env('OTEL_TRACES_SAMPLER_PARENT', true), 63 | 64 | /** 65 | * Sampler type 66 | * Supported values: "always_on", "always_off", "traceidratio" 67 | */ 68 | 'type' => env('OTEL_TRACES_SAMPLER_TYPE', 'always_on'), 69 | 70 | 'args' => [ 71 | /** 72 | * Sampling ratio for traceidratio sampler 73 | */ 74 | 'ratio' => env('OTEL_TRACES_SAMPLER_TRACEIDRATIO_RATIO', 0.05), 75 | ], 76 | ], 77 | ], 78 | 79 | /** 80 | * OpenTelemetry logs configuration 81 | */ 82 | 'logs' => [ 83 | /** 84 | * Logs exporter 85 | * This should be the key of one of the exporters defined in the exporters section 86 | * Supported drivers: "otlp", "console", "null" 87 | */ 88 | 'exporter' => env('OTEL_LOGS_EXPORTER', 'otlp'), 89 | 90 | /** 91 | * Inject active trace id in log context 92 | * 93 | * When using the OpenTelemetry logger, the trace id is always injected in the exported log record. 94 | * This option allows to inject the trace id in the log context for other loggers. 95 | */ 96 | 'inject_trace_id' => true, 97 | 98 | /** 99 | * Context field name for trace id 100 | */ 101 | 'trace_id_field' => 'traceid', 102 | ], 103 | 104 | /** 105 | * OpenTelemetry exporters 106 | * 107 | * Here you can configure exports used by traces and logs. 108 | * If you want to use the same protocol with different endpoints, 109 | * you can copy the exporter with a different and change the endpoint 110 | * 111 | * Supported drivers: "otlp", "zipkin", "console", "null" 112 | */ 113 | 'exporters' => [ 114 | 'otlp' => [ 115 | 'driver' => 'otlp', 116 | 'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318'), 117 | // Supported: "grpc", "http/protobuf", "http/json" 118 | 'protocol' => env('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/protobuf'), 119 | 'traces_timeout' => env('OTEL_EXPORTER_OTLP_TRACES_TIMEOUT', env('OTEL_EXPORTER_OTLP_TIMEOUT', 10000)), 120 | 'metrics_timeout' => env('OTEL_EXPORTER_OTLP_METRICS_TIMEOUT', env('OTEL_EXPORTER_OTLP_TIMEOUT', 10000)), 121 | 'logs_timeout' => env('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', env('OTEL_EXPORTER_OTLP_TIMEOUT', 10000)), 122 | 'max_retries' => env('OTEL_EXPORTER_OTLP_MAX_RETRIES', 3), 123 | ], 124 | 125 | 'zipkin' => [ 126 | 'driver' => 'zipkin', 127 | 'endpoint' => env('OTEL_EXPORTER_ZIPKIN_ENDPOINT', 'http://localhost:9411'), 128 | 'timeout' => env('OTEL_EXPORTER_ZIPKIN_TIMEOUT', 10000), 129 | 'max_retries' => env('OTEL_EXPORTER_ZIPKIN_MAX_RETRIES', 3), 130 | ], 131 | ], 132 | 133 | /** 134 | * List of instrumentation used for application tracing 135 | */ 136 | 'instrumentation' => [ 137 | Instrumentation\HttpServerInstrumentation::class => [ 138 | 'enabled' => env('OTEL_INSTRUMENTATION_HTTP_SERVER', true), 139 | 'excluded_paths' => [], 140 | 'allowed_headers' => [], 141 | 'sensitive_headers' => [], 142 | ], 143 | 144 | Instrumentation\HttpClientInstrumentation::class => [ 145 | 'enabled' => env('OTEL_INSTRUMENTATION_HTTP_CLIENT', true), 146 | 'allowed_headers' => [], 147 | 'sensitive_headers' => [], 148 | ], 149 | 150 | Instrumentation\QueryInstrumentation::class => env('OTEL_INSTRUMENTATION_QUERY', true), 151 | 152 | Instrumentation\RedisInstrumentation::class => env('OTEL_INSTRUMENTATION_REDIS', true), 153 | 154 | Instrumentation\QueueInstrumentation::class => env('OTEL_INSTRUMENTATION_QUEUE', true), 155 | 156 | Instrumentation\CacheInstrumentation::class => env('OTEL_INSTRUMENTATION_CACHE', true), 157 | 158 | Instrumentation\EventInstrumentation::class => [ 159 | 'enabled' => env('OTEL_INSTRUMENTATION_EVENT', true), 160 | 'ignored' => [], 161 | ], 162 | ], 163 | ]; 164 | ``` 165 | 166 | ## Traces 167 | 168 | This package provides a set of integrations to automatically trace common operations in a Laravel application. 169 | You can disable or customize each integration in the config file in the `instrumentations` section. 170 | 171 | ### Provided tracing integrations 172 | 173 | - [Http server requests](#http-server-requests) 174 | - [Http client](#http-client) 175 | - [Database](#database) 176 | - [Redis](#redis) 177 | - [Queue jobs](#redis) 178 | - [Logs context](#logs-context) 179 | - [Manual traces](#manual-traces) 180 | 181 | ### Http server requests 182 | 183 | Http server requests are automatically traced by injecting `\Keepsuit\LaravelOpenTelemetry\Support\HttpServer\TraceRequestMiddleware::class` to the global middlewares. 184 | You can disable it by setting `OT_INSTRUMENTATION_HTTP_SERVER` to `false` or removing the `HttpServerInstrumentation::class` from the config file. 185 | 186 | Configuration options: 187 | 188 | - `excluded_paths`: list of paths to exclude from tracing 189 | - `allowed_headers`: list of headers to include in the trace 190 | - `sensitive_headers`: list of headers with sensitive data to hide in the trace 191 | 192 | ### Http client 193 | 194 | To trace an outgoing http request call the `withTrace` method on the request builder. 195 | 196 | ```php 197 | Http::withTrace()->get('https://example.com'); 198 | ``` 199 | 200 | You can disable it by setting `OT_INSTRUMENTATION_HTTP_CLIENT` to `false` or removing the `HttpClientInstrumentation::class` from the config file. 201 | 202 | Configuration options: 203 | 204 | - `allowed_headers`: list of headers to include in the trace 205 | - `sensitive_headers`: list of headers with sensitive data to hide in the trace 206 | 207 | ### Database 208 | 209 | Database queries are automatically traced. 210 | You can disable it by setting `OT_INSTRUMENTATION_QUERY` to `false` or removing the `QueryInstrumentation::class` from the config file. 211 | 212 | ### Redis 213 | 214 | Redis commands are automatically traced. 215 | You can disable it by setting `OT_INSTRUMENTATION_REDIS` to `false` or removing the `RedisInstrumentation::class` from the config file. 216 | 217 | ### Queue jobs 218 | 219 | Queue jobs are automatically traced. 220 | It will automatically create a parent span with kind `PRODUCER` when a job is dispatched and a child span with kind `CONSUMER` when the job is executed. 221 | You can disable it by setting `OT_INSTRUMENTATION_QUEUE` to `false` or removing the `QueueInstrumentation::class` from the config file. 222 | 223 | ### Logs context 224 | 225 | When starting a trace with provided instrumentation, the trace id is automatically injected in the log context. 226 | This allows to correlate logs with traces. 227 | 228 | If you are starting the root trace manually, 229 | you should call `Tracer::updateLogContext()` to inject the trace id in the log context. 230 | 231 | > [!NOTE] 232 | > When using the OpenTelemetry logs driver (`otlp`), 233 | > the trace id is automatically injected in the log context without the need to call `Tracer::updateLogContext()`. 234 | 235 | ### Manual traces 236 | 237 | Spans can be manually created with the `newSpan` method on the `Tracer` facade. 238 | This method returns a `SpanBuilder` instance that can be used to customize and start the span. 239 | 240 | The simplest way to create a custom trace is with `measure` method: 241 | 242 | ```php 243 | use Keepsuit\LaravelOpenTelemetry\Facades\Tracer; 244 | 245 | Tracer::newSpan('my custom trace')->measure(function () { 246 | // do something 247 | }); 248 | ``` 249 | 250 | Alternatively you can manage the span manually: 251 | 252 | ```php 253 | use Keepsuit\LaravelOpenTelemetry\Facades\Tracer; 254 | 255 | $span = Tracer::newSpan('my custom trace')->start(); 256 | 257 | // do something 258 | 259 | $span->end(); 260 | ``` 261 | 262 | With `measure` the span is automatically set to active (so it will be used as parent for new spans). 263 | With `start` you have to manually set the span as active: 264 | 265 | ```php 266 | use Keepsuit\LaravelOpenTelemetry\Facades\Tracer; 267 | 268 | $span = Tracer::newSpan('my custom trace')->start(); 269 | $scope = $span->activate() 270 | 271 | // do something 272 | 273 | $scope->detach(); 274 | $span->end(); 275 | ``` 276 | 277 | Other utility methods are available on the `Tracer` facade: 278 | 279 | ```php 280 | use Keepsuit\LaravelOpenTelemetry\Facades\Tracer; 281 | 282 | Tracer::traceId(); // get the active trace id 283 | Tracer::activeSpan(); // get the active span 284 | Tracer::activeScope(); // get the active scope 285 | Tracer::currentContext(); // get the current trace context (useful for advanced use cases) 286 | Tracer::propagationHeaders(); // get the propagation headers required to propagate the trace to other services 287 | Tracer::extractContextFromPropagationHeaders(array $headers); // extract the trace context from propagation headers 288 | ``` 289 | 290 | ## Logs 291 | 292 | This package provides a custom log channel that allows to process logs with OpenTelemetry instrumentation. 293 | This packages injects a log channel named `otlp` that can be used to send logs to OpenTelemetry using laravel default log system. 294 | 295 | ```php 296 | // config/logging.php 297 | 'channels' => [ 298 | // injected channel config, you can override it adding an `otlp` channel in your config 299 | 'otlp' => [ 300 | 'driver' => 'monolog', 301 | 'handler' => \Keepsuit\LaravelOpenTelemetry\Support\OpenTelemetryMonologHandler::class, 302 | 'level' => 'debug', 303 | ] 304 | ] 305 | ``` 306 | 307 | As an alternative, you can use the `Logger` facade to send logs directly to OpenTelemetry: 308 | 309 | ```php 310 | use Keepsuit\LaravelOpenTelemetry\Facades\Logger; 311 | 312 | Logger::emergency('my log message'); 313 | Logger::alert('my log message'); 314 | Logger::critical('my log message'); 315 | Logger::error('my log message'); 316 | Logger::warning('my log message'); 317 | Logger::notice('my log message'); 318 | Logger::info('my log message'); 319 | Logger::debug('my log message'); 320 | ``` 321 | 322 | ## Testing 323 | 324 | ```bash 325 | composer test 326 | ``` 327 | 328 | ## Changelog 329 | 330 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 331 | 332 | ## Credits 333 | 334 | - [Fabio Capucci](https://github.com/keepsuit) 335 | - [All Contributors](../../contributors) 336 | 337 | ## License 338 | 339 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 340 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepsuit/laravel-opentelemetry", 3 | "description": "OpenTelemetry integration for laravel", 4 | "keywords": [ 5 | "keepsuit", 6 | "laravel", 7 | "opentelemetry" 8 | ], 9 | "homepage": "https://github.com/keepsuit/laravel-opentelemetry", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Fabio Capucci", 14 | "email": "f.capucci@keepsuit.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", 21 | "illuminate/support": "^10.0 || ^11.0 || ^12.0", 22 | "open-telemetry/api": "^1.1", 23 | "open-telemetry/context": "^1.1", 24 | "open-telemetry/exporter-otlp": "^1.1", 25 | "open-telemetry/sdk": "^1.1", 26 | "open-telemetry/sem-conv": "^1.28.0", 27 | "spatie/laravel-package-tools": "^1.16", 28 | "thecodingmachine/safe": "^2.0 || ^3.0" 29 | }, 30 | "require-dev": { 31 | "guzzlehttp/guzzle": "^7.3", 32 | "guzzlehttp/test-server": "^0.1.0", 33 | "larastan/larastan": "^2.9 || ^3.0", 34 | "laravel/pint": "^1.18", 35 | "nesbot/carbon": "^2.69 || ^3.0", 36 | "nunomaduro/collision": "^7.0 || ^8.0 || ^9.0", 37 | "open-telemetry/exporter-zipkin": "^1.1", 38 | "open-telemetry/extension-propagator-b3": "^1.1", 39 | "open-telemetry/transport-grpc": "^1.1", 40 | "orchestra/testbench": "^8.22 || ^9.0 || ^10.0", 41 | "pestphp/pest": "^2.36 || ^3.0", 42 | "pestphp/pest-plugin-laravel": "^2.4 || ^3.0", 43 | "php-http/guzzle7-adapter": "^1.0", 44 | "phpstan/extension-installer": "^1.4", 45 | "phpstan/phpstan-deprecation-rules": "^1.2 || ^2.0", 46 | "predis/predis": "^2.2", 47 | "spatie/invade": "^2.0", 48 | "spatie/laravel-ray": "^1.40", 49 | "spatie/test-time": "^1.3", 50 | "spatie/valuestore": "^1.3", 51 | "thecodingmachine/phpstan-safe-rule": "^1.2" 52 | }, 53 | "suggest": { 54 | "open-telemetry/extension-propagator-b3": "Required to use B3 propagator", 55 | "open-telemetry/exporter-zipkin": "Required to Zipkin exporter", 56 | "open-telemetry/transport-grpc": "Required to use OTLP gRPC exporter" 57 | }, 58 | "conflict": { 59 | "open-telemetry/extension-propagator-b3": "<1.1.0", 60 | "open-telemetry/exporter-zipkin": "<1.1.0", 61 | "open-telemetry/transport-grpc": "<1.1.0" 62 | }, 63 | "autoload": { 64 | "psr-4": { 65 | "Keepsuit\\LaravelOpenTelemetry\\": "src" 66 | } 67 | }, 68 | "autoload-dev": { 69 | "psr-4": { 70 | "Keepsuit\\LaravelOpenTelemetry\\Tests\\": "tests" 71 | } 72 | }, 73 | "scripts": { 74 | "test": "pest --no-coverage", 75 | "test-coverage": "pest --coverage-html coverage", 76 | "lint": [ 77 | "pint", 78 | "phpstan" 79 | ] 80 | }, 81 | "config": { 82 | "sort-packages": true, 83 | "allow-plugins": { 84 | "composer/package-versions-deprecated": true, 85 | "pestphp/pest-plugin": true, 86 | "php-http/discovery": false, 87 | "phpstan/extension-installer": true, 88 | "tbachert/spi": true 89 | } 90 | }, 91 | "extra": { 92 | "laravel": { 93 | "providers": [ 94 | "Keepsuit\\LaravelOpenTelemetry\\LaravelOpenTelemetryServiceProvider" 95 | ] 96 | } 97 | }, 98 | "minimum-stability": "dev", 99 | "prefer-stable": true 100 | } 101 | -------------------------------------------------------------------------------- /config/opentelemetry.php: -------------------------------------------------------------------------------- 1 | env(Variables::OTEL_SERVICE_NAME, \Illuminate\Support\Str::slug((string) env('APP_NAME', 'laravel-app'))), 11 | 12 | /** 13 | * Comma separated list of propagators to use. 14 | * Supports any otel propagator, for example: "tracecontext", "baggage", "b3", "b3multi", "none" 15 | */ 16 | 'propagators' => env(Variables::OTEL_PROPAGATORS, 'tracecontext'), 17 | 18 | /** 19 | * OpenTelemetry Traces configuration 20 | */ 21 | 'traces' => [ 22 | /** 23 | * Traces exporter 24 | * This should be the key of one of the exporters defined in the exporters section 25 | */ 26 | 'exporter' => env(Variables::OTEL_TRACES_EXPORTER, 'otlp'), 27 | 28 | /** 29 | * Traces sampler 30 | */ 31 | 'sampler' => [ 32 | /** 33 | * Wraps the sampler in a parent based sampler 34 | */ 35 | 'parent' => env('OTEL_TRACES_SAMPLER_PARENT', true), 36 | 37 | /** 38 | * Sampler type 39 | * Supported values: "always_on", "always_off", "traceidratio" 40 | */ 41 | 'type' => env('OTEL_TRACES_SAMPLER_TYPE', 'always_on'), 42 | 43 | 'args' => [ 44 | /** 45 | * Sampling ratio for traceidratio sampler 46 | */ 47 | 'ratio' => env('OTEL_TRACES_SAMPLER_TRACEIDRATIO_RATIO', 0.05), 48 | ], 49 | ], 50 | ], 51 | 52 | /** 53 | * OpenTelemetry logs configuration 54 | */ 55 | 'logs' => [ 56 | /** 57 | * Logs exporter 58 | * This should be the key of one of the exporters defined in the exporters section 59 | * Supported drivers: "otlp", "console", "null" 60 | */ 61 | 'exporter' => env(Variables::OTEL_LOGS_EXPORTER, 'otlp'), 62 | 63 | /** 64 | * Inject active trace id in log context 65 | * 66 | * When using the OpenTelemetry logger, the trace id is always injected in the exported log record. 67 | * This option allows to inject the trace id in the log context for other loggers. 68 | */ 69 | 'inject_trace_id' => true, 70 | 71 | /** 72 | * Context field name for trace id 73 | */ 74 | 'trace_id_field' => 'traceid', 75 | ], 76 | 77 | /** 78 | * OpenTelemetry exporters 79 | * 80 | * Here you can configure exports used by traces and logs. 81 | * If you want to use the same protocol with different endpoints, 82 | * you can copy the exporter with a different and change the endpoint 83 | * 84 | * Supported drivers: "otlp", "zipkin", "console", "null" 85 | */ 86 | 'exporters' => [ 87 | 'otlp' => [ 88 | 'driver' => 'otlp', 89 | 'endpoint' => env(Variables::OTEL_EXPORTER_OTLP_ENDPOINT, 'http://localhost:4318'), 90 | 'protocol' => env(Variables::OTEL_EXPORTER_OTLP_PROTOCOL, 'http/protobuf'), 91 | 'traces_timeout' => env(Variables::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, env(Variables::OTEL_EXPORTER_OTLP_TIMEOUT, 10000)), 92 | 'traces_headers' => (string) env(Variables::OTEL_EXPORTER_OTLP_TRACES_HEADERS, env(Variables::OTEL_EXPORTER_OTLP_HEADERS, '')), 93 | 'metrics_timeout' => env(Variables::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, env(Variables::OTEL_EXPORTER_OTLP_TIMEOUT, 10000)), 94 | 'metrics_headers' => (string) env(Variables::OTEL_EXPORTER_OTLP_METRICS_HEADERS, env(Variables::OTEL_EXPORTER_OTLP_HEADERS, '')), 95 | 'logs_timeout' => env(Variables::OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, env(Variables::OTEL_EXPORTER_OTLP_TIMEOUT, 10000)), 96 | 'logs_headers' => (string) env(Variables::OTEL_EXPORTER_OTLP_LOGS_HEADERS, env(Variables::OTEL_EXPORTER_OTLP_HEADERS, '')), 97 | 'max_retries' => env('OTEL_EXPORTER_OTLP_MAX_RETRIES', 3), 98 | ], 99 | 100 | 'zipkin' => [ 101 | 'driver' => 'zipkin', 102 | 'endpoint' => env(Variables::OTEL_EXPORTER_ZIPKIN_ENDPOINT, 'http://localhost:9411'), 103 | 'timeout' => env(Variables::OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10000), 104 | 'max_retries' => env('OTEL_EXPORTER_ZIPKIN_MAX_RETRIES', 3), 105 | ], 106 | ], 107 | 108 | /** 109 | * List of instrumentation used for application tracing 110 | */ 111 | 'instrumentation' => [ 112 | Instrumentation\HttpServerInstrumentation::class => [ 113 | 'enabled' => env('OTEL_INSTRUMENTATION_HTTP_SERVER', true), 114 | 'excluded_paths' => [], 115 | 'allowed_headers' => [], 116 | 'sensitive_headers' => [], 117 | ], 118 | 119 | Instrumentation\HttpClientInstrumentation::class => [ 120 | 'enabled' => env('OTEL_INSTRUMENTATION_HTTP_CLIENT', true), 121 | 'allowed_headers' => [], 122 | 'sensitive_headers' => [], 123 | ], 124 | 125 | Instrumentation\QueryInstrumentation::class => env('OTEL_INSTRUMENTATION_QUERY', true), 126 | 127 | Instrumentation\RedisInstrumentation::class => env('OTEL_INSTRUMENTATION_REDIS', true), 128 | 129 | Instrumentation\QueueInstrumentation::class => env('OTEL_INSTRUMENTATION_QUEUE', true), 130 | 131 | Instrumentation\CacheInstrumentation::class => env('OTEL_INSTRUMENTATION_CACHE', true), 132 | 133 | Instrumentation\EventInstrumentation::class => [ 134 | 'enabled' => env('OTEL_INSTRUMENTATION_EVENT', true), 135 | 'ignored' => [], 136 | ], 137 | ], 138 | ]; 139 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | - config 6 | tmpDir: build/phpstan 7 | checkOctaneCompatibility: true 8 | checkModelProperties: true 9 | treatPhpDocTypesAsCertain: false 10 | 11 | ignoreErrors: 12 | - identifier: missingType.generics 13 | - identifier: missingType.iterableValue 14 | - identifier: larastan.noEnvCallsOutsideOfConfig 15 | path: config/* 16 | -------------------------------------------------------------------------------- /src/Facades/Logger.php: -------------------------------------------------------------------------------- 1 | listen(CacheHit::class, [$this, 'recordCacheHit']); 16 | app('events')->listen(CacheMissed::class, [$this, 'recordCacheMiss']); 17 | 18 | app('events')->listen(KeyWritten::class, [$this, 'recordCacheSet']); 19 | app('events')->listen(KeyForgotten::class, [$this, 'recordCacheForget']); 20 | } 21 | 22 | public function recordCacheHit(CacheHit $event): void 23 | { 24 | $this->addEvent('cache hit', [ 25 | 'key' => $event->key, 26 | 'tags' => \Safe\json_encode($event->tags), 27 | ]); 28 | } 29 | 30 | public function recordCacheMiss(CacheMissed $event): void 31 | { 32 | $this->addEvent('cache miss', [ 33 | 'key' => $event->key, 34 | 'tags' => \Safe\json_encode($event->tags), 35 | ]); 36 | } 37 | 38 | public function recordCacheSet(KeyWritten $event): void 39 | { 40 | $ttl = $event->seconds ?? 0; 41 | 42 | $this->addEvent('cache set', [ 43 | 'key' => $event->key, 44 | 'tags' => \Safe\json_encode($event->tags), 45 | 'expires_at' => $ttl > 0 ? now()->addSeconds($ttl)->getTimestamp() : 'never', 46 | 'expires_in_seconds' => $ttl > 0 ? $ttl : 'never', 47 | 'expires_in_human' => $ttl > 0 ? now()->addSeconds($ttl)->diffForHumans() : 'never', 48 | ]); 49 | } 50 | 51 | public function recordCacheForget(KeyForgotten $event): void 52 | { 53 | $this->addEvent('cache forget', [ 54 | 'key' => $event->key, 55 | 'tags' => \Safe\json_encode($event->tags), 56 | ]); 57 | } 58 | 59 | private function addEvent(string $name, iterable $attributes = []): void 60 | { 61 | Tracer::activeSpan()->addEvent($name, $attributes); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Instrumentation/EventInstrumentation.php: -------------------------------------------------------------------------------- 1 | listen('*', [$this, 'recordEvent']); 21 | } 22 | 23 | public function recordEvent(string $event, array $payload): void 24 | { 25 | if ($this->isInternalLaravelEvent($event) || $this->isIgnoredEvent($event)) { 26 | return; 27 | } 28 | 29 | Tracer::activeSpan()->addEvent(sprintf('Event %s fired', $event), [ 30 | 'event.name' => $event, 31 | ]); 32 | } 33 | 34 | protected function isInternalLaravelEvent(string $event): bool 35 | { 36 | return Str::is([ 37 | 'Illuminate\*', 38 | 'Laravel\Octane\*', 39 | 'Laravel\Scout\*', 40 | 'eloquent*', 41 | 'bootstrapped*', 42 | 'bootstrapping*', 43 | 'creating*', 44 | 'composing*', 45 | ], $event); 46 | } 47 | 48 | protected function isIgnoredEvent(string $event): bool 49 | { 50 | return in_array($event, static::$ignoredEvents); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Instrumentation/HandlesHttpHeaders.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $defaultSensitiveHeaders = [ 16 | 'authorization', 17 | 'php-auth-pw', 18 | 'cookie', 19 | 'set-cookie', 20 | ]; 21 | 22 | /** 23 | * @var array 24 | */ 25 | protected static array $allowedHeaders = []; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected static array $sensitiveHeaders = []; 31 | 32 | /** 33 | * @return array 34 | */ 35 | public static function getAllowedHeaders(): array 36 | { 37 | return static::$allowedHeaders; 38 | } 39 | 40 | public static function headerIsAllowed(string $header): bool 41 | { 42 | return in_array($header, static::getAllowedHeaders()); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public static function getSensitiveHeaders(): array 49 | { 50 | return static::$sensitiveHeaders; 51 | } 52 | 53 | public static function headerIsSensitive(string $header): bool 54 | { 55 | return in_array($header, static::getSensitiveHeaders()); 56 | } 57 | 58 | /** 59 | * @param array $headers 60 | * @return array 61 | */ 62 | protected function normalizeHeaders(array $headers): array 63 | { 64 | return Arr::map( 65 | $headers, 66 | fn (string $header) => strtolower(trim($header)), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Instrumentation/HttpClientInstrumentation.php: -------------------------------------------------------------------------------- 1 | normalizeHeaders(Arr::get($options, 'allowed_headers', [])); 16 | 17 | static::$sensitiveHeaders = array_merge( 18 | $this->normalizeHeaders(Arr::get($options, 'sensitive_headers', [])), 19 | $this->defaultSensitiveHeaders 20 | ); 21 | 22 | $this->registerWithTraceMacro(); 23 | } 24 | 25 | protected function registerWithTraceMacro(): void 26 | { 27 | PendingRequest::macro('withTrace', function () { 28 | /** @var PendingRequest $this */ 29 | return $this->withMiddleware(GuzzleTraceMiddleware::make()); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Instrumentation/HttpServerInstrumentation.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public static function getExcludedPaths(): array 19 | { 20 | return static::$excludedPaths; 21 | } 22 | 23 | public function register(array $options): void 24 | { 25 | static::$excludedPaths = array_map( 26 | fn (string $path) => ltrim($path, '/'), 27 | Arr::get($options, 'excluded_paths', []) 28 | ); 29 | 30 | static::$allowedHeaders = $this->normalizeHeaders(Arr::get($options, 'allowed_headers', [])); 31 | 32 | static::$sensitiveHeaders = array_merge( 33 | $this->normalizeHeaders(Arr::get($options, 'sensitive_headers', [])), 34 | $this->defaultSensitiveHeaders 35 | ); 36 | 37 | $this->injectMiddleware(app(Kernel::class)); 38 | } 39 | 40 | protected function injectMiddleware(Kernel $kernel): void 41 | { 42 | if (! $kernel instanceof \Illuminate\Foundation\Http\Kernel) { 43 | return; 44 | } 45 | 46 | if (! $kernel->hasMiddleware(TraceRequestMiddleware::class)) { 47 | $kernel->prependMiddleware(TraceRequestMiddleware::class); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Instrumentation/Instrumentation.php: -------------------------------------------------------------------------------- 1 | listen(QueryExecuted::class, [$this, 'recordQuery']); 19 | } 20 | 21 | public function recordQuery(QueryExecuted $event): void 22 | { 23 | if (! Tracer::traceStarted()) { 24 | return; 25 | } 26 | 27 | $operationName = Str::of($event->sql) 28 | ->before(' ') 29 | ->upper() 30 | ->when( 31 | value: fn (Stringable $operationName) => in_array($operationName, ['SELECT', 'INSERT', 'UPDATE', 'DELETE']), 32 | callback: fn (Stringable $operationName) => $operationName->toString(), 33 | default: fn () => '' 34 | ); 35 | 36 | if ($operationName === '') { 37 | return; 38 | } 39 | 40 | $span = Tracer::newSpan(sprintf('sql %s', $operationName)) 41 | ->setSpanKind(SpanKind::KIND_CLIENT) 42 | ->setStartTimestamp($this->getEventStartTimestampNs($event->time)) 43 | ->setAttribute(TraceAttributes::DB_SYSTEM_NAME, $event->connection->getDriverName()) 44 | ->setAttribute(TraceAttributes::DB_NAMESPACE, $event->connection->getDatabaseName()) 45 | ->setAttribute(TraceAttributes::DB_OPERATION_NAME, $operationName) 46 | ->setAttribute(TraceAttributes::DB_QUERY_TEXT, $event->sql) 47 | ->setAttribute(TraceAttributes::SERVER_ADDRESS, $event->connection->getConfig('host')) 48 | ->setAttribute(TraceAttributes::SERVER_PORT, $event->connection->getConfig('port')) 49 | ->start(); 50 | 51 | $span->end(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Instrumentation/QueueInstrumentation.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected array $activeSpans = []; 27 | 28 | public function register(array $options): void 29 | { 30 | $this->recordJobQueueing(); 31 | $this->recordJobProcessing(); 32 | } 33 | 34 | protected function recordJobQueueing(): void 35 | { 36 | if (app()->resolved('queue')) { 37 | $this->registerQueueInterceptor(app('queue')); 38 | } else { 39 | app()->afterResolving('queue', fn ($queue) => $this->registerQueueInterceptor($queue)); 40 | } 41 | 42 | app('events')->listen(JobQueued::class, function (JobQueued $event) { 43 | $uuid = $event->payload()['uuid'] ?? null; 44 | 45 | if (! is_string($uuid)) { 46 | return; 47 | } 48 | 49 | $span = $this->activeSpans[$uuid] ?? null; 50 | 51 | $span?->end(); 52 | 53 | unset($this->activeSpans[$uuid]); 54 | }); 55 | } 56 | 57 | protected function registerQueueInterceptor(QueueManager $queue): void 58 | { 59 | try { 60 | $queue->createPayloadUsing(function (string $connection, ?string $queue, array $payload) { 61 | if (! Tracer::traceStarted()) { 62 | return $payload; 63 | } 64 | 65 | $uuid = $payload['uuid']; 66 | 67 | if (! is_string($uuid)) { 68 | return $payload; 69 | } 70 | 71 | $jobName = Arr::get($payload, 'displayName', 'unknown'); 72 | $queueName = Str::after($queue ?? 'default', 'queues:'); 73 | 74 | $span = Tracer::newSpan(sprintf('%s enqueue', $jobName)) 75 | ->setSpanKind(SpanKind::KIND_PRODUCER) 76 | ->setAttribute(TraceAttributes::MESSAGING_SYSTEM, $this->connectionDriver($connection)) 77 | ->setAttribute(TraceAttributes::MESSAGING_OPERATION_TYPE, 'enqueue') 78 | ->setAttribute(TraceAttributes::RPC_MESSAGE_ID, $uuid) 79 | ->setAttribute(TraceAttributes::MESSAGING_DESTINATION_NAME, $queueName) 80 | ->setAttribute(TraceAttributes::MESSAGING_DESTINATION_TEMPLATE, $jobName) 81 | ->start(); 82 | 83 | $context = $span->storeInContext(Tracer::currentContext()); 84 | 85 | $this->activeSpans[$uuid] = $span; 86 | 87 | return Tracer::propagationHeaders($context); 88 | }); 89 | } catch (Throwable $e) { 90 | report($e); 91 | } 92 | } 93 | 94 | protected function recordJobProcessing(): void 95 | { 96 | app('events')->listen(JobProcessing::class, function (JobProcessing $event) { 97 | $context = Tracer::extractContextFromPropagationHeaders($event->job->payload()); 98 | 99 | $span = Tracer::newSpan(sprintf('%s process', $event->job->resolveName())) 100 | ->setSpanKind(SpanKind::KIND_CONSUMER) 101 | ->setParent($context) 102 | ->setAttribute(TraceAttributes::MESSAGING_SYSTEM, $this->connectionDriver($event->connectionName)) 103 | ->setAttribute(TraceAttributes::MESSAGING_OPERATION_TYPE, 'process') 104 | ->setAttribute(TraceAttributes::RPC_MESSAGE_ID, $event->job->uuid()) 105 | ->setAttribute(TraceAttributes::MESSAGING_DESTINATION_NAME, $event->job->getQueue()) 106 | ->setAttribute(TraceAttributes::MESSAGING_DESTINATION_TEMPLATE, $event->job->resolveName()) 107 | ->start(); 108 | 109 | $span->activate(); 110 | 111 | Tracer::updateLogContext(); 112 | }); 113 | 114 | app('events')->listen(JobProcessed::class, function (JobProcessed $event) { 115 | $scope = Tracer::activeScope(); 116 | $span = Tracer::activeSpan(); 117 | 118 | $scope?->detach(); 119 | $span->end(); 120 | }); 121 | 122 | app('events')->listen(JobFailed::class, function (JobFailed $event) { 123 | $scope = Tracer::activeScope(); 124 | $span = Tracer::activeSpan(); 125 | 126 | $span->recordException($event->exception) 127 | ->setStatus(StatusCode::STATUS_ERROR); 128 | 129 | $scope?->detach(); 130 | $span->end(); 131 | }); 132 | } 133 | 134 | protected function connectionDriver(string $connection): string 135 | { 136 | return config(sprintf('queue.connections.%s.driver', $connection), 'unknown'); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Instrumentation/RedisInstrumentation.php: -------------------------------------------------------------------------------- 1 | listen(CommandExecuted::class, [$this, 'recordCommand']); 18 | 19 | if (app()->resolved('redis')) { 20 | $this->registerRedisEvents(app()->make('redis')); 21 | } else { 22 | app()->afterResolving('redis', fn ($redis) => $this->registerRedisEvents($redis)); 23 | } 24 | } 25 | 26 | public function recordCommand(CommandExecuted $event): void 27 | { 28 | if (! Tracer::traceStarted()) { 29 | return; 30 | } 31 | 32 | $traceName = sprintf('redis %s %s', $event->connection->getName(), $event->command); 33 | 34 | $span = Tracer::newSpan($traceName) 35 | ->setSpanKind(SpanKind::KIND_CLIENT) 36 | ->setStartTimestamp($this->getEventStartTimestampNs($event->time)) 37 | ->start(); 38 | 39 | if ($span->isRecording()) { 40 | $span->setAttribute(TraceAttributes::DB_SYSTEM_NAME, 'redis') 41 | ->setAttribute(TraceAttributes::DB_QUERY_TEXT, $this->formatCommand($event->command, $event->parameters)) 42 | ->setAttribute(TraceAttributes::SERVER_ADDRESS, $this->resolveRedisAddress($event->connection->client())); 43 | } 44 | 45 | $span->end(); 46 | } 47 | 48 | protected function resolveRedisAddress(mixed $client): ?string 49 | { 50 | if ($client instanceof \Redis) { 51 | return $client->getHost() ?: null; 52 | } 53 | 54 | if ($client instanceof \Predis\Client) { 55 | $connection = $client->getConnection(); 56 | 57 | return $connection instanceof \Predis\Connection\NodeConnectionInterface 58 | ? ($connection->getParameters()->host ?? null) 59 | : null; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * Format the given Redis command. 67 | */ 68 | protected function formatCommand(string $command, array $parameters): string 69 | { 70 | $parameters = collect($parameters)->map(function ($parameter) { 71 | if (is_array($parameter)) { 72 | return collect($parameter)->map(function ($value, $key) { 73 | if (is_array($value)) { 74 | return \Safe\json_encode($value); 75 | } 76 | 77 | return is_int($key) ? $value : sprintf('%s %s', $key, $value); 78 | })->implode(' '); 79 | } 80 | 81 | return $parameter; 82 | })->implode(' '); 83 | 84 | return sprintf('%s %s', $command, $parameters); 85 | } 86 | 87 | protected function registerRedisEvents(mixed $redis): void 88 | { 89 | if ($redis instanceof RedisManager) { 90 | foreach ((array) $redis->connections() as $connection) { 91 | $connection->setEventDispatcher(app('events')); 92 | } 93 | 94 | $redis->enableEvents(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Instrumentation/SpanTimeAdapter.php: -------------------------------------------------------------------------------- 1 | now(); 13 | $durationNs = (int) ($timeMs * ClockInterface::NANOS_PER_MILLISECOND); 14 | 15 | return $nowNs - $durationNs; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/InstrumentationServiceProvider.php: -------------------------------------------------------------------------------- 1 | $options) { 13 | if ($options === false) { 14 | continue; 15 | } 16 | 17 | if (is_array($options) && ! ($options['enabled'] ?? true)) { 18 | continue; 19 | } 20 | 21 | $watcher = $this->app->make($key); 22 | 23 | if ($watcher instanceof Instrumentation) { 24 | $watcher->register(is_array($options) ? $options : []); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LaravelOpenTelemetryServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureEnvironmentVariables(); 58 | $this->injectConfig(); 59 | $this->init(); 60 | $this->registerInstrumentation(); 61 | } 62 | 63 | public function configurePackage(Package $package): void 64 | { 65 | $package 66 | ->name('laravel-opentelemetry') 67 | ->hasConfigFile(); 68 | } 69 | 70 | protected function init(): void 71 | { 72 | Clock::setDefault(new CarbonClock); 73 | 74 | $resource = ResourceInfoFactory::defaultResource()->merge( 75 | ResourceInfo::create(Attributes::create([ 76 | ResourceAttributes::SERVICE_NAME => config('opentelemetry.service_name'), 77 | ])) 78 | ); 79 | 80 | $propagator = PropagatorBuilder::new()->build(config('opentelemetry.propagators')); 81 | 82 | /** 83 | * Traces 84 | */ 85 | $spanExporter = $this->buildSpanExporter(); 86 | $this->app->bind(SpanExporterInterface::class, fn () => $spanExporter); 87 | $spanProcessor = (new BatchSpanProcessorBuilder($spanExporter))->build(); 88 | 89 | $samplerConfig = config('opentelemetry.traces.sampler', []); 90 | $sampler = SamplerBuilder::new()->build( 91 | $samplerConfig['type'] ?? 'always_on', 92 | $samplerConfig['parent'] ?? true, 93 | $samplerConfig['args'] ?? [] 94 | ); 95 | 96 | $tracerProvider = TracerProvider::builder() 97 | ->setResource($resource) 98 | ->addSpanProcessor($spanProcessor) 99 | ->setSampler($sampler) 100 | ->build(); 101 | 102 | /** 103 | * Logs 104 | */ 105 | $logExporter = $this->buildLogsExporter(); 106 | $this->app->bind(LogRecordExporterInterface::class, fn () => $logExporter); 107 | $logProcessor = new BatchLogRecordProcessor( 108 | exporter: $logExporter, 109 | clock: Clock::getDefault() 110 | ); 111 | 112 | $loggerProvider = LoggerProvider::builder() 113 | ->setResource($resource) 114 | ->addLogRecordProcessor($logProcessor) 115 | ->build(); 116 | 117 | Sdk::builder() 118 | ->setTracerProvider($tracerProvider) 119 | ->setLoggerProvider($loggerProvider) 120 | ->setPropagator($propagator) 121 | ->setAutoShutdown(true) 122 | ->buildAndRegisterGlobal(); 123 | 124 | $instrumentation = new CachedInstrumentation( 125 | name: 'laravel-opentelemetry', 126 | version: class_exists(InstalledVersions::class) ? InstalledVersions::getPrettyVersion('keepsuit/laravel-opentelemetry') : null, 127 | schemaUrl: TraceAttributes::SCHEMA_URL, 128 | ); 129 | 130 | $this->app->bind(TextMapPropagatorInterface::class, fn () => $propagator); 131 | $this->app->bind(TracerInterface::class, fn () => $instrumentation->tracer()); 132 | $this->app->bind(LoggerInterface::class, fn () => $instrumentation->logger()); 133 | 134 | $this->app->terminating(function () use ($loggerProvider, $tracerProvider) { 135 | $tracerProvider->forceFlush(); 136 | $loggerProvider->forceFlush(); 137 | }); 138 | } 139 | 140 | protected function registerInstrumentation(): void 141 | { 142 | if (Sdk::isDisabled()) { 143 | return; 144 | } 145 | 146 | $this->app->booted(function (Application $app) { 147 | $app->register(InstrumentationServiceProvider::class); 148 | }); 149 | } 150 | 151 | private function configureEnvironmentVariables(): void 152 | { 153 | $envRepository = Env::getRepository(); 154 | 155 | $envRepository->set(OTELVariables::OTEL_SERVICE_NAME, config('opentelemetry.service_name')); 156 | 157 | // Disable debug scopes wrapping 158 | $envRepository->set('OTEL_PHP_DEBUG_SCOPES_DISABLED', '1'); 159 | } 160 | 161 | protected function buildSpanExporter(): SpanExporterInterface 162 | { 163 | $tracesExporter = config('opentelemetry.traces.exporter'); 164 | $tracesExporterConfig = config(sprintf('opentelemetry.exporters.%s', $tracesExporter)); 165 | $tracesExporterDriver = is_array($tracesExporterConfig) ? $tracesExporterConfig['driver'] : $tracesExporter; 166 | 167 | return match ($tracesExporterDriver) { 168 | 'zipkin' => $this->buildZipkinExporter($tracesExporterConfig ?? []), 169 | 'otlp' => new OtlpSpanExporter($this->buildOtlpTransport($tracesExporterConfig ?? [], Signals::TRACE)), 170 | 'console' => (new ConsoleSpanExporterFactory)->create(), 171 | default => (new InMemorySpanExporterFactory)->create(), 172 | }; 173 | } 174 | 175 | protected function buildLogsExporter(): LogRecordExporterInterface 176 | { 177 | $logsExporter = config('opentelemetry.logs.exporter'); 178 | $logsExporterConfig = config(sprintf('opentelemetry.exporters.%s', $logsExporter)); 179 | $logsExporterDriver = is_array($logsExporterConfig) ? $logsExporterConfig['driver'] : $logsExporter; 180 | 181 | return match ($logsExporterDriver) { 182 | 'otlp' => new LogsExporter($this->buildOtlpTransport($logsExporterConfig ?? [], Signals::LOGS)), 183 | 'console' => (new LogsConsoleExporterFactory)->create(), 184 | default => (new LogsInMemoryExporterFactory)->create() 185 | }; 186 | } 187 | 188 | /** 189 | * @phpstan-param Signals::TRACE|Signals::METRICS|Signals::LOGS $signal 190 | */ 191 | protected function buildOtlpTransport(array $config, string $signal): TransportInterface 192 | { 193 | $protocol = $config['protocol'] ?? null; 194 | $endpoint = $config['endpoint'] ?? 'http://localhost:4318'; 195 | 196 | $maxRetries = $config['max_retries'] ?? 3; 197 | 198 | $timeoutMillis = match ($signal) { 199 | Signals::TRACE => $config['traces_timeout'] ?? 10000, 200 | Signals::METRICS => $config['metrics_timeout'] ?? 10000, 201 | Signals::LOGS => $config['logs_timeout'] ?? 10000, 202 | }; 203 | 204 | $headers = match ($signal) { 205 | Signals::TRACE => $config['traces_headers'] ?? [], 206 | Signals::METRICS => $config['metrics_headers'] ?? [], 207 | Signals::LOGS => $config['logs_headers'] ?? [], 208 | }; 209 | 210 | $headers = rescue( 211 | fn () => is_string($headers) ? MapParser::parse($headers) : $headers, 212 | [], 213 | report: false 214 | ); 215 | 216 | return match ($protocol) { 217 | 'grpc' => (new GrpcTransportFactory)->create( 218 | endpoint: $endpoint.OtlpUtil::method($signal), 219 | headers: $headers, 220 | maxRetries: $maxRetries, 221 | ), 222 | 'http/json', 'json' => (new OtlpHttpTransportFactory)->create( 223 | endpoint: (new HttpEndpointResolver)->resolveToString($endpoint, $signal), 224 | contentType: 'application/json', 225 | headers: $headers, 226 | timeout: $timeoutMillis / 1000, 227 | maxRetries: $maxRetries 228 | ), 229 | default => (new OtlpHttpTransportFactory)->create( 230 | endpoint: (new HttpEndpointResolver)->resolveToString($endpoint, $signal), 231 | contentType: 'application/x-protobuf', 232 | headers: $headers, 233 | timeout: $timeoutMillis / 1000, 234 | maxRetries: $maxRetries, 235 | ), 236 | }; 237 | } 238 | 239 | protected function buildZipkinExporter(array $config): ZipkinExporter 240 | { 241 | $endpoint = Str::of(Arr::get($config, 'endpoint', ''))->rtrim('/')->append('/api/v2/spans')->toString(); 242 | $maxRetries = $config['max_retries'] ?? 3; 243 | $timeoutMillis = $config['timeout'] ?? 10000; 244 | 245 | return new ZipkinExporter( 246 | (new PsrTransportFactory( 247 | Psr18ClientDiscovery::find(), 248 | Psr17FactoryDiscovery::findRequestFactory(), 249 | Psr17FactoryDiscovery::findStreamFactory(), 250 | ))->create( 251 | endpoint: $endpoint, 252 | contentType: 'application/json', 253 | timeout: $timeoutMillis / 1000, 254 | maxRetries: $maxRetries, 255 | ), 256 | ); 257 | } 258 | 259 | protected function injectConfig(): void 260 | { 261 | $this->callAfterResolving(Repository::class, function (Repository $config) { 262 | if ($config->has('logging.channels.otlp')) { 263 | return; 264 | } 265 | 266 | $config->set('logging.channels.otlp', [ 267 | 'driver' => 'monolog', 268 | 'handler' => OpenTelemetryMonologHandler::class, 269 | 'level' => 'debug', 270 | ]); 271 | }); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | log(LogLevel::EMERGENCY, $message, $context); 20 | } 21 | 22 | public function alert(string $message, array $context = []): void 23 | { 24 | $this->log(LogLevel::ALERT, $message, $context); 25 | } 26 | 27 | public function critical(string $message, array $context = []): void 28 | { 29 | $this->log(LogLevel::CRITICAL, $message, $context); 30 | } 31 | 32 | public function error(string $message, array $context = []): void 33 | { 34 | $this->log(LogLevel::ERROR, $message, $context); 35 | } 36 | 37 | public function warning(string $message, array $context = []): void 38 | { 39 | $this->log(LogLevel::WARNING, $message, $context); 40 | } 41 | 42 | public function notice(string $message, array $context = []): void 43 | { 44 | $this->log(LogLevel::NOTICE, $message, $context); 45 | } 46 | 47 | public function info(string $message, array $context = []): void 48 | { 49 | $this->log(LogLevel::INFO, $message, $context); 50 | } 51 | 52 | public function debug(string $message, array $context = []): void 53 | { 54 | $this->log(LogLevel::DEBUG, $message, $context); 55 | } 56 | 57 | /** 58 | * @phpstan-param LogLevel::* $level 59 | */ 60 | public function log(string $level, string $message, array $context = []): void 61 | { 62 | $logRecord = (new LogRecord($message)) 63 | ->setTimestamp(Clock::getDefault()->now()) 64 | ->setSeverityNumber(Severity::fromPsr3($level)) 65 | ->setSeverityText($level); 66 | 67 | foreach ($context as $key => $value) { 68 | $logRecord->setAttribute($key, $value); 69 | } 70 | 71 | $this->logger->emit($logRecord); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Support/CarbonClock.php: -------------------------------------------------------------------------------- 1 | systemClock = new SystemClock; 18 | } 19 | 20 | public function now(): int 21 | { 22 | if (Carbon::hasTestNow()) { 23 | return static::carbonToNanos(CarbonImmutable::now()); 24 | } 25 | 26 | return $this->systemClock->now(); 27 | } 28 | 29 | public static function carbonToNanos(CarbonInterface $carbon): int 30 | { 31 | return (int) $carbon->getPreciseTimestamp(6) * 1000; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Support/HttpClient/GuzzleTraceMiddleware.php: -------------------------------------------------------------------------------- 1 | getMethod())) 27 | ->setSpanKind(SpanKind::KIND_CLIENT) 28 | ->setAttribute(TraceAttributes::URL_FULL, sprintf('%s://%s%s', $request->getUri()->getScheme(), $request->getUri()->getHost(), $request->getUri()->getPath())) 29 | ->setAttribute(TraceAttributes::URL_PATH, $request->getUri()->getPath()) 30 | ->setAttribute(TraceAttributes::URL_QUERY, $request->getUri()->getQuery()) 31 | ->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $request->getMethod()) 32 | ->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $request->getBody()->getSize()) 33 | ->setAttribute(TraceAttributes::URL_SCHEME, $request->getUri()->getScheme()) 34 | ->setAttribute(TraceAttributes::SERVER_ADDRESS, $request->getUri()->getHost()) 35 | ->setAttribute(TraceAttributes::SERVER_PORT, $request->getUri()->getPort()) 36 | ->start(); 37 | 38 | static::recordHeaders($span, $request); 39 | 40 | $context = $span->storeInContext(Tracer::currentContext()); 41 | 42 | foreach (Tracer::propagationHeaders($context) as $key => $value) { 43 | $request = $request->withHeader($key, $value); 44 | } 45 | 46 | $promise = $handler($request, $options); 47 | assert($promise instanceof PromiseInterface); 48 | 49 | return $promise->then(function (ResponseInterface $response) use ($span) { 50 | $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode()); 51 | 52 | if (($contentLength = $response->getHeader('Content-Length')[0] ?? null) !== null) { 53 | $span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $contentLength); 54 | } 55 | 56 | static::recordHeaders($span, $response); 57 | 58 | if ($response->getStatusCode() >= 400) { 59 | $span->setStatus(StatusCode::STATUS_ERROR); 60 | } 61 | 62 | $span->end(); 63 | 64 | return $response; 65 | }); 66 | }; 67 | }; 68 | } 69 | 70 | protected static function recordHeaders(SpanInterface $span, RequestInterface|ResponseInterface $http): SpanInterface 71 | { 72 | $prefix = match (true) { 73 | $http instanceof RequestInterface => 'http.request.header.', 74 | $http instanceof ResponseInterface => 'http.response.header.', 75 | }; 76 | 77 | foreach ($http->getHeaders() as $key => $value) { 78 | $key = strtolower($key); 79 | 80 | if (! HttpClientInstrumentation::headerIsAllowed($key)) { 81 | continue; 82 | } 83 | 84 | $value = HttpClientInstrumentation::headerIsSensitive($key) ? ['*****'] : $value; 85 | 86 | $span->setAttribute($prefix.$key, $value); 87 | } 88 | 89 | return $span; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Support/HttpServer/TraceRequestMiddleware.php: -------------------------------------------------------------------------------- 1 | is(HttpServerInstrumentation::getExcludedPaths())) { 21 | return $next($request); 22 | } 23 | 24 | $span = $this->startTracing($request); 25 | $scope = $span->activate(); 26 | 27 | Tracer::updateLogContext(); 28 | 29 | try { 30 | $response = $next($request); 31 | 32 | if ($response instanceof Response) { 33 | $this->recordHttpResponseToSpan($span, $response); 34 | } 35 | 36 | return $response; 37 | } catch (\Throwable $exception) { 38 | $span->recordException($exception) 39 | ->setStatus(StatusCode::STATUS_ERROR); 40 | 41 | throw $exception; 42 | } finally { 43 | $scope->detach(); 44 | $span->end(); 45 | } 46 | } 47 | 48 | protected function startTracing(Request $request): SpanInterface 49 | { 50 | $context = Tracer::extractContextFromPropagationHeaders($request->headers->all()); 51 | 52 | /** @var non-empty-string $route */ 53 | $route = rescue(fn () => Route::getRoutes()->match($request)->uri(), $request->path(), false); 54 | $route = str_starts_with($route, '/') ? $route : '/'.$route; 55 | 56 | $span = Tracer::newSpan($route) 57 | ->setSpanKind(SpanKind::KIND_SERVER) 58 | ->setParent($context) 59 | ->setAttribute(TraceAttributes::URL_FULL, $request->fullUrl()) 60 | ->setAttribute(TraceAttributes::URL_PATH, $request->path() === '/' ? $request->path() : '/'.$request->path()) 61 | ->setAttribute(TraceAttributes::URL_QUERY, $request->getQueryString()) 62 | ->setAttribute(TraceAttributes::URL_SCHEME, $request->getScheme()) 63 | ->setAttribute(TraceAttributes::HTTP_ROUTE, $route) 64 | ->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $request->method()) 65 | ->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $request->header('Content-Length')) 66 | ->setAttribute(TraceAttributes::SERVER_ADDRESS, $request->getHttpHost()) 67 | ->setAttribute(TraceAttributes::SERVER_PORT, $request->getPort()) 68 | ->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent()) 69 | ->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $request->getProtocolVersion()) 70 | ->setAttribute(TraceAttributes::NETWORK_PEER_ADDRESS, $request->ip()) 71 | ->start(); 72 | 73 | $this->recordHeaders($span, $request); 74 | 75 | return $span; 76 | } 77 | 78 | protected function recordHttpResponseToSpan(SpanInterface $span, Response $response): void 79 | { 80 | $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode()); 81 | 82 | if (($content = $response->getContent()) !== false) { 83 | $span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, strlen($content)); 84 | } 85 | 86 | $this->recordHeaders($span, $response); 87 | 88 | if ($response->isSuccessful()) { 89 | $span->setStatus(StatusCode::STATUS_OK); 90 | } 91 | 92 | if ($response->isServerError() || $response->isClientError()) { 93 | $span->setStatus(StatusCode::STATUS_ERROR); 94 | } 95 | } 96 | 97 | protected function recordHeaders(SpanInterface $span, Request|Response $http): SpanInterface 98 | { 99 | $prefix = match (true) { 100 | $http instanceof Request => 'http.request.header.', 101 | $http instanceof Response => 'http.response.header.', 102 | }; 103 | 104 | foreach ($http->headers->all() as $key => $value) { 105 | $key = strtolower($key); 106 | 107 | if (! HttpServerInstrumentation::headerIsAllowed($key)) { 108 | continue; 109 | } 110 | 111 | $value = HttpServerInstrumentation::headerIsSensitive($key) ? ['*****'] : $value; 112 | 113 | $span->setAttribute($prefix.$key, $value); 114 | } 115 | 116 | return $span; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Support/OpenTelemetryMonologHandler.php: -------------------------------------------------------------------------------- 1 | level->toPsrLogLevel(); 14 | 15 | Logger::log($level, $record->message, $record->context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/PropagatorBuilder.php: -------------------------------------------------------------------------------- 1 | buildPropagator($propagators[0]); 30 | } 31 | 32 | return new MultiTextMapPropagator(array_map(fn (string $name) => $this->buildPropagator($name), $propagators)); 33 | } 34 | 35 | protected function buildPropagator(string $name): TextMapPropagatorInterface 36 | { 37 | try { 38 | return Registry::textMapPropagator($name); 39 | } catch (\RuntimeException $e) { 40 | Log::warning($e->getMessage()); 41 | } 42 | 43 | return NoopTextMapPropagator::getInstance(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/SamplerBuilder.php: -------------------------------------------------------------------------------- 1 | buildSampler(strtolower(trim($sampler)), $args); 21 | 22 | if ($parentBased) { 23 | return new ParentBased($instance); 24 | } 25 | 26 | return $instance; 27 | } 28 | 29 | protected function buildSampler(string $name, array $args): SamplerInterface 30 | { 31 | return match ($name) { 32 | 'always_on' => new AlwaysOnSampler, 33 | 'traceidratio' => new TraceIdRatioBasedSampler($args['ratio'] ?? 0.05), 34 | default => new AlwaysOffSampler, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Support/SpanBuilder.php: -------------------------------------------------------------------------------- 1 | spanBuilder->setParent($context); 24 | 25 | return $this; 26 | } 27 | 28 | public function addLink(SpanContextInterface $context, iterable $attributes = []): SpanBuilder 29 | { 30 | $this->spanBuilder->addLink($context, $attributes); 31 | 32 | return $this; 33 | } 34 | 35 | public function setAttribute(string $key, mixed $value): SpanBuilder 36 | { 37 | $this->spanBuilder->setAttribute($key, $value); 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param iterable $attributes 44 | */ 45 | public function setAttributes(iterable $attributes): SpanBuilder 46 | { 47 | $this->spanBuilder->setAttributes($attributes); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @param CarbonInterface|int $timestamp A carbon instance or a timestamp in nanoseconds 54 | */ 55 | public function setStartTimestamp(CarbonInterface|int $timestamp): SpanBuilder 56 | { 57 | if ($timestamp instanceof CarbonInterface) { 58 | $timestamp = CarbonClock::carbonToNanos($timestamp); 59 | } 60 | 61 | $this->spanBuilder->setStartTimestamp($timestamp); 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @phpstan-param SpanKind::KIND_* $spanKind 68 | */ 69 | public function setSpanKind(int $spanKind): SpanBuilder 70 | { 71 | $this->spanBuilder->setSpanKind($spanKind); 72 | 73 | return $this; 74 | } 75 | 76 | public function start(): SpanInterface 77 | { 78 | return $this->spanBuilder->startSpan(); 79 | } 80 | 81 | /** 82 | * @template U 83 | * 84 | * @param Closure(SpanInterface $span): U $callback 85 | * @return (U is PendingDispatch ? null : U) 86 | * 87 | * @throws Throwable 88 | */ 89 | public function measure(Closure $callback): mixed 90 | { 91 | $span = $this->start(); 92 | $scope = $span->activate(); 93 | 94 | try { 95 | $result = $callback($span); 96 | 97 | // Fix: Dispatch is effective only on destruct 98 | if ($result instanceof PendingDispatch) { 99 | $result = null; 100 | } 101 | 102 | return $result; 103 | } catch (Throwable $exception) { 104 | $span->recordException($exception); 105 | 106 | throw $exception; 107 | } finally { 108 | $scope->detach(); 109 | $span->end(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Tracer.php: -------------------------------------------------------------------------------- 1 | activeSpan()->getContext()->isValid(); 26 | } 27 | 28 | public function currentContext(): ContextInterface 29 | { 30 | return Context::getCurrent(); 31 | } 32 | 33 | public function activeScope(): ?ScopeInterface 34 | { 35 | return Context::storage()->scope(); 36 | } 37 | 38 | public function activeSpan(): SpanInterface 39 | { 40 | return Span::getCurrent(); 41 | } 42 | 43 | public function traceId(): ?string 44 | { 45 | $traceId = $this->activeSpan()->getContext()->getTraceId(); 46 | 47 | return SpanContextValidator::isValidTraceId($traceId) ? $traceId : null; 48 | } 49 | 50 | /** 51 | * @phpstan-param non-empty-string $name 52 | */ 53 | public function newSpan(string $name): SpanBuilder 54 | { 55 | return new SpanBuilder($this->tracer->spanBuilder($name)); 56 | } 57 | 58 | public function propagationHeaders(?ContextInterface $context = null): array 59 | { 60 | $headers = []; 61 | 62 | $this->propagator->inject($headers, context: $context); 63 | 64 | return $headers; 65 | } 66 | 67 | public function extractContextFromPropagationHeaders(array $headers): ContextInterface 68 | { 69 | return $this->propagator->extract($headers); 70 | } 71 | 72 | public function updateLogContext(): void 73 | { 74 | if (! config('opentelemetry.logs.inject_trace_id', true)) { 75 | return; 76 | } 77 | 78 | $traceId = $this->traceId(); 79 | 80 | if ($traceId === null) { 81 | return; 82 | } 83 | 84 | $field = config('opentelemetry.logs.trace_id_field', 'traceid'); 85 | 86 | Log::shareContext([ 87 | $field => $traceId, 88 | ]); 89 | } 90 | } 91 | --------------------------------------------------------------------------------