├── .editorconfig-checker.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docs ├── .gitignore ├── package-lock.json ├── package.json ├── patches │ └── sass-embedded+1.85.1.patch └── src │ ├── .vuepress │ ├── config.js │ └── public │ │ └── .no-jekyll │ ├── README.md │ ├── how-to │ └── docker-env-setup.md │ ├── instrumentation │ ├── introduction.md │ ├── logs.md │ ├── metrics.md │ └── traces.md │ └── user-guide │ ├── configuration.md │ ├── getting-started.md │ ├── introduction.md │ └── troubleshooting.md └── src ├── DependencyInjection ├── Compiler │ ├── CacheInstrumentationPass.php │ ├── HttpClientInstrumentationPass.php │ ├── SetConsoleTracingExcludeCommandsPass.php │ ├── SetHttpKernelTracingExcludePathsPass.php │ ├── SetInstrumentationTypePass.php │ └── TracerLocatorPass.php ├── Configuration.php ├── OpenTelemetryExtension.php ├── OpenTelemetryLogsExtension.php ├── OpenTelemetryMetricsExtension.php └── OpenTelemetryTracesExtension.php ├── Instrumentation ├── Attribute │ └── Traceable.php ├── Doctrine │ └── Middleware │ │ ├── TraceableConnectionV3.php │ │ ├── TraceableConnectionV4.php │ │ ├── TraceableDriverV3.php │ │ ├── TraceableDriverV4.php │ │ ├── TraceableMiddleware.php │ │ ├── TraceableStatementV3.php │ │ ├── TraceableStatementV4.php │ │ └── Tracer.php ├── InstrumentationTypeEnum.php ├── InstrumentationTypeInterface.php ├── Symfony │ ├── Cache │ │ ├── TagAwareTraceableCacheAdapter.php │ │ ├── TraceableCacheAdapter.php │ │ ├── TraceableCacheAdapterTrait.php │ │ └── Tracer.php │ ├── Console │ │ ├── ObservableConsoleEventSubscriber.php │ │ └── TraceableConsoleEventSubscriber.php │ ├── Framework │ │ └── Routing │ │ │ └── TraceableRouteLoader.php │ ├── HttpClient │ │ ├── TraceableHttpClient.php │ │ └── TraceableResponse.php │ ├── HttpKernel │ │ ├── ObservableHttpKernelEventSubscriber.php │ │ └── TraceableHttpKernelEventSubscriber.php │ ├── Mailer │ │ ├── TraceableMailer.php │ │ └── TraceableMailerTransport.php │ └── Messenger │ │ ├── TraceableMessengerMiddleware.php │ │ ├── TraceableMessengerStack.php │ │ ├── TraceableMessengerTransport.php │ │ ├── TraceableMessengerTransportFactory.php │ │ └── TransportTracer.php └── Twig │ └── TraceableTwigExtension.php ├── OpenTelemetry ├── Context │ ├── Attribute │ │ ├── ConsoleTraceAttributeEnum.php │ │ ├── DoctrineTraceAttributeEnum.php │ │ └── HttpKernelTraceAttributeEnum.php │ └── Propagator │ │ └── HeadersPropagator.php ├── Exporter │ ├── ConsoleExporterEndpoint.php │ ├── EmptyExporterOptions.php │ ├── ExporterDsn.php │ ├── ExporterEndpointInterface.php │ ├── ExporterFactoryInterface.php │ ├── ExporterOptionsInterface.php │ ├── OtlpExporterCompressionEnum.php │ ├── OtlpExporterEndpoint.php │ ├── OtlpExporterFormatEnum.php │ └── OtlpExporterOptions.php ├── Log │ ├── LogExporter │ │ ├── AbstractLogExporterFactory.php │ │ ├── ConsoleLogExporterFactory.php │ │ ├── InMemoryLogExporterFactory.php │ │ ├── LogExporterEnum.php │ │ ├── LogExporterFactory.php │ │ ├── LogExporterFactoryInterface.php │ │ ├── NoopLogExporterFactory.php │ │ └── OtlpLogExporterFactory.php │ ├── LogExporterEndpoint.php │ ├── LogProcessor │ │ ├── AbstractLogProcessorFactory.php │ │ ├── BatchLogProcessorFactory.php │ │ ├── LogProcessorEnum.php │ │ ├── LogProcessorFactoryInterface.php │ │ ├── MultiLogProcessorFactory.php │ │ ├── NoopLogProcessorFactory.php │ │ └── SimpleLogProcessorFactory.php │ └── LoggerProvider │ │ ├── AbstractLoggerProviderFactory.php │ │ ├── DefaultLoggerProviderFactory.php │ │ ├── LoggerProviderEnum.php │ │ ├── LoggerProviderFactoryInterface.php │ │ └── NoopLoggerProviderFactory.php ├── Metric │ ├── ExemplarFilterEnum.php │ ├── ExemplarFilterFactory.php │ ├── MeterProvider │ │ ├── AbstractMeterProviderFactory.php │ │ ├── DefaultMeterProviderFactory.php │ │ ├── MeterProviderEnum.php │ │ ├── MeterProviderFactoryInterface.php │ │ └── NoopMeterProviderFactory.php │ ├── MetricExporter │ │ ├── AbstractMetricExporterFactory.php │ │ ├── ConsoleMetricExporterFactory.php │ │ ├── InMemoryMetricExporterFactory.php │ │ ├── MetricExporterEnum.php │ │ ├── MetricExporterFactory.php │ │ ├── MetricExporterFactoryInterface.php │ │ ├── MetricTemporalityEnum.php │ │ ├── NoopMetricExporterFactory.php │ │ └── OtlpMetricExporterFactory.php │ ├── MetricExporterEndpoint.php │ └── MetricExporterOptions.php ├── Resource │ └── ResourceInfoFactory.php ├── Trace │ ├── Sampler │ │ └── AttributeBasedSampler.php │ ├── SamplerFactory.php │ ├── SpanExporter │ │ ├── AbstractSpanExporterFactory.php │ │ ├── ConsoleSpanExporterFactory.php │ │ ├── InMemorySpanExporterFactory.php │ │ ├── OtlpSpanExporterFactory.php │ │ ├── SpanExporterFactory.php │ │ ├── SpanExporterFactoryInterface.php │ │ ├── TraceExporterEnum.php │ │ └── ZipkinSpanExporterFactory.php │ ├── SpanProcessor │ │ ├── AbstractSpanProcessorFactory.php │ │ ├── MultiSpanProcessorFactory.php │ │ ├── NoopSpanProcessorFactory.php │ │ ├── SimpleSpanProcessorFactory.php │ │ ├── SpanProcessorEnum.php │ │ └── SpanProcessorFactoryInterface.php │ ├── TraceExporterEndpoint.php │ ├── TraceSamplerEnum.php │ ├── TracerProvider │ │ ├── AbstractTracerProviderFactory.php │ │ ├── DefaultTracerProviderFactory.php │ │ ├── NoopTracerProviderFactory.php │ │ ├── TraceProviderEnum.php │ │ └── TracerProviderFactoryInterface.php │ └── ZipkinExporterEndpoint.php └── Transport │ ├── AbstractTransportFactory.php │ ├── GrpcTransportFactory.php │ ├── OtlpHttpTransportFactory.php │ ├── PsrHttpTransportFactory.php │ ├── StreamTransportFactory.php │ ├── TransportEnum.php │ ├── TransportFactory.php │ ├── TransportFactoryInterface.php │ └── TransportParams.php ├── OpenTelemetryBundle.php ├── Resources └── config │ ├── services.php │ ├── services_logs.php │ ├── services_metering_instrumentation.php │ ├── services_metrics.php │ ├── services_traces.php │ ├── services_tracing_instrumentation.php │ └── services_transports.php └── aliases.php /.editorconfig-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Verbose": false, 3 | "Debug": false, 4 | "IgnoreDefaults": false, 5 | "SpacesAfterTabs": false, 6 | "NoColor": false, 7 | "Exclude": [ 8 | "flake.lock", 9 | "package.json", 10 | "package-lock.json", 11 | "composer.lock", 12 | "tests/*" 13 | ], 14 | "AllowedContentTypes": [], 15 | "PassedFiles": [], 16 | "Disable": { 17 | "EndOfLine": false, 18 | "Indentation": false, 19 | "InsertFinalNewline": false, 20 | "TrimTrailingWhitespace": false, 21 | "IndentSize": false, 22 | "MaxLineLength": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See conventional commits for commit guidelines. 4 | 5 | --- 6 | 7 | ## Unreleased 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gaël Reyrol 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 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .temp 3 | .cache 4 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "scripts": { 5 | "build": "vuepress build src", 6 | "clean-dev": "vuepress dev src --clean-cache", 7 | "dev": "vuepress dev src", 8 | "update-package": "npx vp-update", 9 | "postinstall": "patch-package" 10 | }, 11 | "devDependencies": { 12 | "@vuepress/bundler-vite": "2.0.0-rc.20", 13 | "@vuepress/plugin-copy-code": "2.0.0-rc.80", 14 | "@vuepress/plugin-git": "2.0.0-rc.79", 15 | "@vuepress/plugin-prismjs": "2.0.0-rc.80", 16 | "@vuepress/theme-default": "2.0.0-rc.80", 17 | "patch-package": "^8.0.0", 18 | "sass-embedded": "^1.85.1", 19 | "vuepress": "2.0.0-rc.20" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/patches/sass-embedded+1.85.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/sass-embedded/dist/lib/src/compiler-path.js b/node_modules/sass-embedded/dist/lib/src/compiler-path.js 2 | index ae33aa3..7a49d16 100644 3 | --- a/node_modules/sass-embedded/dist/lib/src/compiler-path.js 4 | +++ b/node_modules/sass-embedded/dist/lib/src/compiler-path.js 5 | @@ -24,6 +24,10 @@ function isLinuxMusl(path) { 6 | } 7 | /** The full command for the embedded compiler executable. */ 8 | exports.compilerCommand = (() => { 9 | + const binPath = process.env.SASS_EMBEDDED_BIN_PATH; 10 | + if (binPath) { 11 | + return [binPath]; 12 | + } 13 | const platform = process.platform === 'linux' && isLinuxMusl(process.execPath) 14 | ? 'linux-musl' 15 | : process.platform; 16 | -------------------------------------------------------------------------------- /docs/src/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | import {defaultTheme} from '@vuepress/theme-default' 2 | import {defineUserConfig} from 'vuepress' 3 | import {viteBundler} from '@vuepress/bundler-vite' 4 | import { prismjsPlugin } from '@vuepress/plugin-prismjs' 5 | import {copyCodePlugin} from '@vuepress/plugin-copy-code' 6 | import {gitPlugin} from '@vuepress/plugin-git' 7 | 8 | const isProd = process.env.NODE_ENV === 'production' 9 | 10 | export default defineUserConfig({ 11 | lang: 'en-US', 12 | base: isProd ? '/opentelemetry-bundle/' : '/', 13 | 14 | title: 'OpenTelemetry Bundle', 15 | description: 'Traces, metrics, and logs instrumentation within your Symfony application', 16 | 17 | theme: defaultTheme({ 18 | repo: 'https://github.com/FriendsOfOpenTelemetry/opentelemetry-bundle', 19 | docsRepo: 'https://github.com/FriendsOfOpenTelemetry/opentelemetry-bundle', 20 | docsDir: 'docs/src', 21 | lastUpdated: true, 22 | 23 | navbar: [ 24 | { 25 | text: 'User Guide', 26 | children: [ 27 | '/user-guide/introduction.md', 28 | '/user-guide/getting-started.md', 29 | '/user-guide/configuration.md', 30 | '/user-guide/troubleshooting.md', 31 | ], 32 | }, 33 | { 34 | text: 'Instrumentation', 35 | children: [ 36 | '/instrumentation/introduction.md', 37 | '/instrumentation/traces.md', 38 | '/instrumentation/metrics.md', 39 | '/instrumentation/logs.md', 40 | ], 41 | }, 42 | { 43 | text: 'How To', 44 | children: [ 45 | '/how-to/docker-env-setup.md', 46 | ], 47 | }, 48 | ], 49 | 50 | sidebarDepth: 2, 51 | sidebar: [ 52 | { 53 | text: 'User Guide', 54 | children: [ 55 | '/user-guide/introduction.md', 56 | '/user-guide/getting-started.md', 57 | '/user-guide/configuration.md', 58 | '/user-guide/troubleshooting.md', 59 | ], 60 | }, 61 | { 62 | text: 'Instrumentation', 63 | children: [ 64 | '/instrumentation/introduction.md', 65 | '/instrumentation/traces.md', 66 | '/instrumentation/metrics.md', 67 | '/instrumentation/logs.md', 68 | ], 69 | }, 70 | { 71 | text: 'How To', 72 | children: [ 73 | '/how-to/docker-env-setup.md', 74 | ], 75 | } 76 | ], 77 | }), 78 | 79 | plugins: [ 80 | prismjsPlugin(), 81 | copyCodePlugin(), 82 | gitPlugin(), 83 | ], 84 | 85 | bundler: viteBundler({ 86 | viteOptions: { 87 | css: { 88 | preprocessorOptions: { 89 | scss: { 90 | api: 'modern-compiler', 91 | } 92 | } 93 | } 94 | } 95 | }), 96 | }) 97 | -------------------------------------------------------------------------------- /docs/src/.vuepress/public/.no-jekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfOpenTelemetry/opentelemetry-bundle/b868778a512526430f00af5f2f29f1cf0da215b4/docs/src/.vuepress/public/.no-jekyll -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | tagline: Traces, metrics and logs instrumentation within your Symfony application 4 | actions: 5 | - text: Get Started 6 | link: /user-guide/getting-started.html 7 | type: primary 8 | - text: Introduction 9 | link: /user-guide/introduction.html 10 | type: secondary 11 | footer: MIT Licensed | Copyright © 2023-present Gaël Reyrol 12 | --- 13 | -------------------------------------------------------------------------------- /docs/src/how-to/docker-env-setup.md: -------------------------------------------------------------------------------- 1 | # Set up a Docker environment 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/src/instrumentation/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Instrumentation consist of allowing the OpenTelemetry SDK to automatically generate telemetry data for your Symfony application. 4 | 5 | A Symfony application can use several components provided by Symfony such as the console, the cache etc. 6 | 7 | Each component require a specific instrumentation to be able to generate telemetry data. 8 | Some of them require decoration to be instrumented, others require to listen to Symfony events or extra configuration. 9 | 10 | > **Note**: The instrumentation is still in alpha, only generate traces and may not cover all the use cases. 11 | > Please open a new issue or a pull request if you find an uncovered use case. 12 | 13 | Here is how each component is "hooked" to allow instrumentation: 14 | 15 | - Cache: Cache services are instrumented by decorating services tagged with `cache.taggable` and `cache.pool` tags. 16 | - Console: Commands are instrumented by registering an event subscriber on `ConsoleEvents` events. 17 | - Doctrine: A middleware is registered to wrap DBAL methods such as `prepare`, `query`, `beginTransaction`, `commit`, `rollBack` etc. 18 | - Http Client: The client services are instrumented by decorated services tagged with `http_client.transport` or `http_client` tags. 19 | - Http Kernel: Controllers are instrumented by registering an event subscriber on `KernelEvents` events. 20 | - Mailer: The mailer services are instrumented by decorating services tagged with `mailer.mailer` tag. 21 | - The transport is also instrumented by decorating services tagged with `mailer.transports` and `mailer.default_transports` tag. 22 | - Messenger: A message bus can be instrumented by configuring a new middleware called `open_telemetry_tracer`. 23 | - The transport can also be instrumented by wrapping the DSN string with `trace()`. 24 | - Twig: Rendering is instrumented by registering a Twig extension that adds a `ProfilerNodeVisitor` to the Twig environment. 25 | 26 | Each component comes with a configuration block that allows to enable or disable the instrumentation and configure it. 27 | 28 | ```yaml 29 | open_telemetry: 30 | instrumentation: 31 | : 32 | tracing: 33 | enabled: true 34 | tracer: 'open_telemetry.traces.tracers.main' 35 | ``` 36 | 37 | It is important to understand that the current implementation rely on entrypoint components, which create root spans to allow "secondary" components to attach their spans to it. 38 | A secondary component that does not have a root span will create orphan traces and mess with your obsa, as they won't be attached to any root span. 39 | 40 | In a Symfony application the entrypoint components are the `HttpKernel`, the `Console` and `Messenger`. 41 | 42 | - The `HttpKernel` is the entrypoint for requests. 43 | - The `Console` is the entrypoint for commands. 44 | - The `Messenger` is the entrypoint for messages. 45 | 46 | Those entrypoint components can be configured in two ways: 47 | 48 | - Automatically: The bundle will automatically instrument all registered routes and commands. 49 | - Manually: Only instrument routes and commands using the `#[Traceable]` attribute. 50 | 51 | For further information on tracing instrumentation, please refer to the [Traces](/instrumentation/traces.md) documentation. 52 | -------------------------------------------------------------------------------- /docs/src/instrumentation/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/src/instrumentation/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/src/instrumentation/traces.md: -------------------------------------------------------------------------------- 1 | # Traces 2 | 3 | What OpenTelemetry documentation says about Traces: 4 | > Traces give us the big picture of what happens when a request is made to an application. Whether your application is a monolith with a single database or a sophisticated mesh of services, traces are essential to understanding the full “path” a request takes in your application. 5 | 6 | ## Configuration 7 | 8 | To configure traces, you need to define a `tracer`, a `provider`, a `processor` and an `exporter`. 9 | 10 | Here is a basic example: 11 | 12 | ```yaml 13 | open_telemetry: 14 | traces: 15 | tracers: 16 | main: 17 | # A tracer must refer a provider using the service id pattern `open_telemetry.traces.providers.`. 18 | provider: 'open_telemetry.traces.providers.default' 19 | providers: 20 | default: 21 | type: default 22 | sampler: always_on 23 | processors: 24 | # A provider must refer one or more processor using the service id pattern `open_telemetry.traces.processors.`. 25 | - 'open_telemetry.traces.processors.simple' 26 | processors: 27 | simple: 28 | type: simple 29 | # A processor must refer an exporter using the service id pattern `open_telemetry.traces.exporters.`. 30 | exporter: 'open_telemetry.traces.exporters.otlp' 31 | exporters: 32 | otlp: 33 | dsn: http+otlp://localhost 34 | ``` 35 | 36 | A service with the following id `open_telemetry.traces.tracers.default_tracer` is automatically defined and reference the first tracer registered in your configuration. 37 | This `default_tracer` will be injected in your services using the `OpenTelemetry\API\Trace\TracerInterface` interface. 38 | 39 | An exporter DSN is a string that follows the following pattern: `?transport+exporter://[user:password@]host[:port][/path][?query]`. 40 | 41 | A DSN starts with a transport and an exporter separated by a `+` character. The transport might be optional depending on the exporter. 42 | 43 | Here is table list of the available transport and exporter for traces: 44 | 45 | | Transport | Exporter | Description | Example | Default | 46 | |-----------|-----------|--------------------------------------------------------------|-------------------------------------------|--------------| 47 | | http(s) | otlp | OpenTelemetry exporter using HTTP protocol (over TLS) | http+otlp://localhost:4318/v1/traces | N/A | 48 | | grpc(s) | otlp | OpenTelemetry exporter using gRPC protocol (over TLS) | grpc+otlp://localhost:4317 | N/A | 49 | | http(s) | zipkin | Zipkin exporter using HTTP protocol (over TLS) | http+zipkin://localhost:9411/api/v2/spans | N/A | 50 | | empty | in-memory | In-memory exporter for testing purpose | in-memory://default | N/A | 51 | | stream | console | Console exporter for testing purpose using a stream resource | stream+console://default | php://stdout | 52 | 53 | Note: The `stream+console` DSN is the only DSN than can refer to a stream resource using the `path` block. For example: `stream+console://default/file.log`. 54 | 55 | To trace a specific part of your application, please refer to the documentation of the OpenTelemetry PHP SDK Traces section, [here](https://opentelemetry.io/docs/languages/php/instrumentation/#traces). 56 | 57 | ## Components 58 | 59 | Here is the list of the available Symfony components that can be traced: 60 | 61 | - Cache 62 | - Console 63 | - Doctrine 64 | - Http Client 65 | - Http Kernel 66 | - Mailer 67 | - Messenger 68 | - Twig 69 | 70 | Each component can be configured using the following configuration block: 71 | 72 | ```yaml 73 | open_telemetry: 74 | instrumentation: 75 | : 76 | tracing: 77 | enabled: true # Default: false 78 | tracer: 'open_telemetry.traces.tracers.main' # Default: 'open_telemetry.traces.tracers.default_tracer' 79 | # ... 80 | ``` 81 | 82 | Once you enabled an instrumentation, it will automatically create spans, based on its tracer, provider, processor and exporter. 83 | 84 | With the `Console` and `HttpKernel` entrypoint components, you can also define a `type` configuration block: 85 | 86 | ```yaml 87 | open_telemetry: 88 | instrumentation: 89 | console: 90 | type: attribute # Default: auto 91 | tracing: 92 | # ... 93 | ``` 94 | 95 | The `type` option allows you to define how the instrumentation is done. The following options are available: 96 | 97 | - `auto`: Automatically instrument all registered routes and commands. 98 | - `attribute`: Only instrument routes and commands using the `#[Traceable]` attribute. 99 | 100 | Here is an example of how to use the `#[Traceable]` attribute: 101 | 102 | ```php 103 | use FriendsOfOpenTelemetry\OpenTelemetryBundle\Attribute\Traceable; 104 | 105 | #[Traceable] 106 | #[AsCommand('test')] 107 | class TestCommand extends Command 108 | { 109 | // ... 110 | } 111 | 112 | #[Traceable] 113 | class TestController extends AbstractController 114 | { 115 | #[Traceable] 116 | #[Route('/test', name: 'test')] 117 | public function test(): Response 118 | { 119 | // ... 120 | } 121 | } 122 | ``` 123 | 124 | You can define the following options for the `#[Traceable]` attribute: 125 | 126 | - `tracer`: The service id of the tracer to use (e.g. `open_telemetry.traces.tracers.main`) 127 | If no tracer is defined, the default tracer will be used. 128 | -------------------------------------------------------------------------------- /docs/src/user-guide/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | To get a full list of configuration options, run the following command: 4 | 5 | ```bash 6 | bin/console config:dump-reference open_telemetry 7 | ``` 8 | 9 | ## Full configuration reference 10 | 11 | @[code](../reference.yaml) 12 | 13 | > **Note**: The configuration reference is generated from the bundle's source code. 14 | > It is always up-to-date with the latest version of the bundle. 15 | -------------------------------------------------------------------------------- /docs/src/user-guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Run the following command to install it in your application: 6 | 7 | ```bash 8 | composer require friendsofopentelemetry/opentelemetry-bundle 9 | ``` 10 | 11 | ### Notes 12 | 13 | #### OpenTelemetry SDK and `tbachert/spi` 14 | 15 | The OpenTelemetry SDK uses `tbachert/spi`, a Composer plugin that registers services by loading files from the autoload configuration. This acts similarly to a container but is not the recommended approach for managing services in Symfony applications. 16 | 17 | To ensure a clean and optimal setup: 18 | 19 | - When prompted during installation, do not enable the `tbachert/spi` plugin. 20 | - Install the `mcaskill/composer-exclude-files` Composer plugin and exclude OpenTelemetry files from the autoload process by adding the following configuration to your composer.json file: 21 | 22 | ```json 23 | { 24 | "extra": { 25 | "exclude-from-files": [ 26 | "open-telemetry/*" 27 | ] 28 | } 29 | } 30 | ``` 31 | 32 | #### HTTP PSR Discovery and `php-http/discovery` 33 | 34 | You may also be prompted to enable the `php-http/discovery` Composer plugin. This plugin allows libraries to discover HTTP PSR implementations dynamically. While this is required by many OpenTelemetry dependencies, our bundle relies directly on Guzzle HTTP for simplicity. 35 | 36 | - Recommendation: Enable the plugin if your application requires it, but this is optional. 37 | - If you believe relying directly on Guzzle HTTP is not ideal, please open an issue to share your feedback. As this package is in active development, we are open to exploring better solutions. 38 | 39 | ### Supported Versions 40 | 41 | There is no stable version yet, so you can use the `dev` version to install the bundle. 42 | 43 | | Version | Branch | PHP | OpenTelemetry | Symfony | 44 | |---------|--------|--------|---------------|---------| 45 | | dev | `main` | `^8.2` | `^1.0` | `^7.0` | 46 | 47 | ## Usage 48 | 49 | This bundle is not yet available using Symfony Flex, so you have to manually configure it. 50 | 51 | In your `config/bundles.php` file, add the following line at the end of the array: 52 | 53 | ```php 54 | return [ 55 | // ... 56 | FriendsOfOpenTelemetry\OpenTelemetryBundle::class => ['all' => true], 57 | ]; 58 | ``` 59 | 60 | Then, create a new file `config/packages/open_telemetry.yaml` and add the following minimal configuration: 61 | 62 | ```yaml 63 | open_telemetry: 64 | service: 65 | namespace: 'MyCompany' 66 | name: 'MyApp' 67 | version: '1.0.0' 68 | environment: '%kernel.environment%' 69 | ``` 70 | 71 | For further details on the configuration, please refer to the [Configuration page](/user-guide/configuration.md). 72 | -------------------------------------------------------------------------------- /docs/src/user-guide/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ::: warning 4 | This documentation is a work in progress. If you can't find what you're looking for, please open an issue or a pull request. 5 | ::: 6 | 7 | OpenTelemetry is an observability framework – an API, SDK, and tools that are designed to aid in the generation and collection of application telemetry data such as metrics, logs, and traces. 8 | 9 | For documentation of the underlying SDK, visit the [OpenTelemetry PHP SDK documentation](https://opentelemetry.io/docs/languages/php/). 10 | 11 | This bundle provides an integration of the OpenTelemetry PHP SDK within your Symfony application. 12 | 13 | - Auto registration of OpenTelemetry services (Traces, Meters & Loggers). 14 | - Auto instrumentation of Symfony components. 15 | 16 | List of Symfony components that can be instrumented: 17 | 18 | - Cache 19 | - Console 20 | - Doctrine 21 | - Http Client 22 | - Http Kernel 23 | - Mailer 24 | - Messenger 25 | - Twig 26 | 27 | ## Should I use this bundle in production? 28 | 29 | No, not yet. This bundle is still in development and should not be used in production or at your own risk. 30 | It misses a few features, and it is not yet tested in a real-world application. It might also not be optimized for performance. 31 | As long as the release goals are not reached and fully defined, no stable version will be released. 32 | But you are welcome to try this bundle and give feedback or contribute to it. 33 | 34 | In case you want to try it, a Symfony demo is maintained on this [repository](https://github.com/FriendsOfOpenTelemetry/symfony-demo). 35 | -------------------------------------------------------------------------------- /docs/src/user-guide/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CacheInstrumentationPass.php: -------------------------------------------------------------------------------- 1 | getParameter('open_telemetry.instrumentation.cache.tracing.enabled')) { 16 | return; 17 | } 18 | 19 | foreach ($container->findTaggedServiceIds('cache.pool') as $serviceId => $tags) { 20 | $cachePoolDefinition = $container->getDefinition($serviceId); 21 | if ($cachePoolDefinition->isAbstract()) { 22 | continue; 23 | } 24 | 25 | $definitionClass = $this->resolveDefinitionClass($container, $cachePoolDefinition); 26 | if (null === $definitionClass) { 27 | continue; 28 | } 29 | 30 | $traceableCachePoolDefinition = new ChildDefinition('open_telemetry.instrumentation.cache.trace.adapter'); 31 | $traceableCachePoolDefinition 32 | ->setDecoratedService($serviceId) 33 | ->setArgument('$adapter', new Reference('.inner')); 34 | 35 | $container->setDefinition($serviceId.'.tracer', $traceableCachePoolDefinition); 36 | } 37 | 38 | foreach ($container->findTaggedServiceIds('cache.taggable') as $serviceId => $tags) { 39 | $cachePoolDefinition = $container->getDefinition($serviceId); 40 | if ($cachePoolDefinition->isAbstract()) { 41 | continue; 42 | } 43 | 44 | $definitionClass = $this->resolveDefinitionClass($container, $cachePoolDefinition); 45 | if (null === $definitionClass) { 46 | continue; 47 | } 48 | 49 | $traceableCachePoolDefinition = new ChildDefinition('open_telemetry.instrumentation.cache.trace.tag_aware_adapter'); 50 | $traceableCachePoolDefinition 51 | ->setDecoratedService($serviceId) 52 | ->setArgument('$adapter', new Reference('.inner')); 53 | 54 | $container->setDefinition($serviceId.'.tracer', $traceableCachePoolDefinition); 55 | } 56 | } 57 | 58 | private function resolveDefinitionClass(ContainerBuilder $container, Definition $definition): ?string 59 | { 60 | $class = $definition->getClass(); 61 | 62 | while (null === $class && $definition instanceof ChildDefinition) { 63 | $definition = $container->findDefinition($definition->getParent()); 64 | $class = $definition->getClass(); 65 | } 66 | 67 | return $class; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/HttpClientInstrumentationPass.php: -------------------------------------------------------------------------------- 1 | getParameter('open_telemetry.instrumentation.http_client.tracing.enabled')) { 14 | return; 15 | } 16 | 17 | $decoratedService = $this->getDecoratedService($container); 18 | if (null === $decoratedService) { 19 | $container->removeDefinition('open_telemetry.instrumentation.http_client.trace.client'); 20 | 21 | return; 22 | } 23 | 24 | $container->getDefinition('open_telemetry.instrumentation.http_client.trace.client') 25 | ->setArgument('$client', new Reference('.inner')) 26 | ->setDecoratedService($decoratedService[0], null, $decoratedService[1]); 27 | } 28 | 29 | /** 30 | * @return array{string, int}|null 31 | */ 32 | private function getDecoratedService(ContainerBuilder $container): ?array 33 | { 34 | if ($container->hasDefinition('http_client.transport')) { 35 | return ['http_client.transport', -15]; 36 | } 37 | 38 | if ($container->hasDefinition('http_client')) { 39 | return ['http_client', 15]; 40 | } 41 | 42 | return null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SetConsoleTracingExcludeCommandsPass.php: -------------------------------------------------------------------------------- 1 | hasParameter('open_telemetry.instrumentation.console.tracing.exclude_commands')) { 13 | return; 14 | } 15 | 16 | if (false === $container->hasDefinition('open_telemetry.instrumentation.console.trace.event_subscriber')) { 17 | return; 18 | } 19 | 20 | $excludeCommands = $container->getParameter('open_telemetry.instrumentation.console.tracing.exclude_commands'); 21 | $container->getDefinition('open_telemetry.instrumentation.console.trace.event_subscriber') 22 | ->addMethodCall('setExcludeCommands', [$excludeCommands]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SetHttpKernelTracingExcludePathsPass.php: -------------------------------------------------------------------------------- 1 | hasParameter('open_telemetry.instrumentation.http_kernel.tracing.exclude_paths')) { 13 | return; 14 | } 15 | 16 | if (false === $container->hasDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber')) { 17 | return; 18 | } 19 | 20 | $excludePaths = $container->getParameter('open_telemetry.instrumentation.http_kernel.tracing.exclude_paths'); 21 | $container->getDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber') 22 | ->addMethodCall('setExcludePaths', [$excludePaths]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SetInstrumentationTypePass.php: -------------------------------------------------------------------------------- 1 | hasParameter('open_telemetry.instrumentation.console.type')) { 13 | $consoleInstrumentationType = $container->getParameter('open_telemetry.instrumentation.console.type'); 14 | if ($container->hasDefinition('open_telemetry.instrumentation.console.trace.event_subscriber')) { 15 | $container->getDefinition('open_telemetry.instrumentation.console.trace.event_subscriber') 16 | ->addMethodCall('setInstrumentationType', [$consoleInstrumentationType]); 17 | } 18 | } 19 | 20 | if ($container->hasParameter('open_telemetry.instrumentation.http_kernel.type')) { 21 | $httpKernelInstrumentationType = $container->getParameter('open_telemetry.instrumentation.http_kernel.type'); 22 | if ($container->hasDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber')) { 23 | $container->getDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber') 24 | ->addMethodCall('setInstrumentationType', [$httpKernelInstrumentationType]); 25 | } 26 | } 27 | 28 | if ($container->hasParameter('open_telemetry.instrumentation.messenger.type')) { 29 | $messengerInstrumentationType = $container->getParameter('open_telemetry.instrumentation.messenger.type'); 30 | if ($container->hasDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber')) { 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/TracerLocatorPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds('open_telemetry.tracer'); 16 | 17 | if (0 < count($tracers)) { 18 | if ($container->hasDefinition('open_telemetry.instrumentation.console.trace.event_subscriber')) { 19 | $traceableConsoleEventSubscriber = $container->getDefinition('open_telemetry.instrumentation.console.trace.event_subscriber'); 20 | $this->setTracerLocatorArgument($container, $traceableConsoleEventSubscriber, $tracers); 21 | } 22 | if ($container->hasDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber')) { 23 | $traceableHttpKernelEventSubscriber = $container->getDefinition('open_telemetry.instrumentation.http_kernel.trace.event_subscriber'); 24 | $this->setTracerLocatorArgument($container, $traceableHttpKernelEventSubscriber, $tracers); 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * @param array $tracers 31 | */ 32 | private function setTracerLocatorArgument(ContainerBuilder $container, Definition $service, array $tracers): void 33 | { 34 | $service->setArgument( 35 | '$tracerLocator', 36 | ServiceLocatorTagPass::register($container, array_map(fn (string $id) => new Reference($id), array_keys($tracers))), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DependencyInjection/OpenTelemetryMetricsExtension.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $config; 21 | 22 | private ContainerBuilder $container; 23 | 24 | /** 25 | * @param array{ 26 | * default_meter?: string, 27 | * meters: array, 28 | * exporters: array, 29 | * providers: array 30 | * }|array $config 31 | */ 32 | public function __invoke(array $config, ContainerBuilder $container): void 33 | { 34 | $this->config = $config; 35 | $this->container = $container; 36 | 37 | foreach ($this->config['exporters'] as $name => $exporter) { 38 | $this->loadMetricExporter($name, $exporter); 39 | } 40 | 41 | foreach ($this->config['providers'] as $name => $provider) { 42 | $this->loadMetricProvider($name, $provider); 43 | } 44 | 45 | foreach ($this->config['meters'] as $name => $meter) { 46 | $this->loadMetricMeter($name, $meter); 47 | } 48 | 49 | $defaultMeter = null; 50 | if (0 < count($this->config['meters'])) { 51 | $defaultMeter = array_key_first($this->config['meters']); 52 | } 53 | 54 | if (null !== $defaultMeter) { 55 | $this->container->setAlias('open_telemetry.metrics.default_meter', new Reference(sprintf('open_telemetry.metrics.meters.%s', $defaultMeter))); 56 | $this->container->setAlias(MeterInterface::class, new Reference(sprintf('open_telemetry.metrics.meters.%s', $defaultMeter))); 57 | } 58 | } 59 | 60 | /** 61 | * @param array{ 62 | * dsn: string, 63 | * temporality: string, 64 | * options?: ExporterOptions 65 | * } $config 66 | */ 67 | private function loadMetricExporter(string $name, array $config): void 68 | { 69 | $dsn = (new ChildDefinition('open_telemetry.exporter_dsn'))->setArguments([$config['dsn']]); 70 | $exporterOptions = (new ChildDefinition('open_telemetry.metric_exporter_options'))->setArguments([ 71 | [ 72 | $config['temporality'], 73 | ...$config['options'] ?? [], 74 | ], 75 | ]); 76 | 77 | $this->container 78 | ->setDefinition( 79 | sprintf('open_telemetry.metrics.exporters.%s', $name), 80 | new ChildDefinition('open_telemetry.metrics.exporter_interface'), 81 | ) 82 | ->setArguments([$dsn, $exporterOptions]); 83 | } 84 | 85 | /** 86 | * @param array{ 87 | * type: string, 88 | * exporter?: string, 89 | * filter?: array{type: string, service_id?: string, options?: array} 90 | * } $config 91 | */ 92 | private function loadMetricProvider(string $name, array $config): void 93 | { 94 | $filter = (new ChildDefinition('open_telemetry.metrics.exemplar_filter_factory')); 95 | 96 | $params = []; 97 | if (isset($config['filter']['type']) && ExemplarFilterEnum::Service->value === $config['filter']['type']) { 98 | if (!array_key_exists('service_id', $config['filter'])) { 99 | throw new \LogicException('To configure an exemplar filter of type service, you must specify the service_id key'); 100 | } 101 | $params['service_id'] = new Reference($config['filter']['service_id']); 102 | } 103 | $filter->setArguments([ 104 | $config['filter']['type'] ?? ExemplarFilterEnum::All->value, 105 | $params, 106 | ]); 107 | 108 | $this->container 109 | ->setDefinition( 110 | sprintf('open_telemetry.metrics.providers.%s', $name), 111 | new ChildDefinition('open_telemetry.metrics.provider_interface'), 112 | ) 113 | ->setFactory([new Reference(sprintf('open_telemetry.metrics.provider_factory.%s', $config['type'])), 'createProvider']) 114 | ->setArguments([ 115 | isset($config['exporter']) ? new Reference($config['exporter']) : null, 116 | $filter, 117 | new Reference('open_telemetry.resource_info'), 118 | ]) 119 | ->addTag('open_telemetry.metrics.provider') 120 | ; 121 | } 122 | 123 | /** 124 | * @param array{ 125 | * provider: string, 126 | * name?: string, 127 | * version?: string, 128 | * } $config 129 | */ 130 | private function loadMetricMeter(string $name, array $config): void 131 | { 132 | $this->container 133 | ->setDefinition( 134 | sprintf('open_telemetry.metrics.meters.%s', $name), 135 | new ChildDefinition('open_telemetry.metrics.meter_interface') 136 | ) 137 | ->setPublic(true) 138 | ->setFactory([new Reference($config['provider']), 'getMeter']) 139 | ->setArguments([ 140 | $config['name'] ?? $this->container->getParameter('open_telemetry.bundle.name'), 141 | $config['version'] ?? $this->container->getParameter('open_telemetry.bundle.version'), 142 | ]); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Instrumentation/Attribute/Traceable.php: -------------------------------------------------------------------------------- 1 | tracer->traceFunction('doctrine.dbal.statement.prepare', function (SpanInterface $span) use ($sql): DriverStatement { 26 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 27 | 28 | return new TraceableStatement(parent::prepare($sql), $this->tracer); 29 | }); 30 | } 31 | 32 | public function query(string $sql): Result 33 | { 34 | return $this->tracer->traceFunction('doctrine.dbal.connection.query', function (SpanInterface $span) use ($sql): Result { 35 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 36 | 37 | return parent::query($sql); 38 | }); 39 | } 40 | 41 | public function exec(string $sql): int 42 | { 43 | return $this->tracer->traceFunction('doctrine.dbal.connection.exec', function (SpanInterface $span) use ($sql): int { 44 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 45 | 46 | return parent::exec($sql); 47 | }); 48 | } 49 | 50 | public function beginTransaction(): bool 51 | { 52 | return $this->tracer->traceFunction('doctrine.dbal.transaction.begin', function (SpanInterface $span): bool { 53 | return parent::beginTransaction(); 54 | }); 55 | } 56 | 57 | public function commit(): bool 58 | { 59 | return $this->tracer->traceFunction('doctrine.dbal.transaction.commit', function (SpanInterface $span): bool { 60 | return parent::commit(); 61 | }); 62 | } 63 | 64 | public function rollBack(): bool 65 | { 66 | return $this->tracer->traceFunction('doctrine.dbal.transaction.rollback', function (SpanInterface $span): bool { 67 | return parent::rollBack(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableConnectionV4.php: -------------------------------------------------------------------------------- 1 | tracer->traceFunction('doctrine.dbal.statement.prepare', function (SpanInterface $span) use ($sql): DriverStatement { 26 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 27 | 28 | return new TraceableStatement(parent::prepare($sql), $this->tracer); 29 | }); 30 | } 31 | 32 | public function query(string $sql): Result 33 | { 34 | return $this->tracer->traceFunction('doctrine.dbal.connection.query', function (SpanInterface $span) use ($sql): Result { 35 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 36 | 37 | return parent::query($sql); 38 | }); 39 | } 40 | 41 | public function exec(string $sql): int 42 | { 43 | return $this->tracer->traceFunction('doctrine.dbal.connection.exec', function (SpanInterface $span) use ($sql): int { 44 | $span->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sql); 45 | 46 | return parent::exec($sql); 47 | }); 48 | } 49 | 50 | public function beginTransaction(): void 51 | { 52 | $this->tracer->traceFunction('doctrine.dbal.transaction.begin', function (SpanInterface $span): void { 53 | parent::beginTransaction(); 54 | }); 55 | } 56 | 57 | public function commit(): void 58 | { 59 | $this->tracer->traceFunction('doctrine.dbal.transaction.commit', function (SpanInterface $span): void { 60 | parent::commit(); 61 | }); 62 | } 63 | 64 | public function rollBack(): void 65 | { 66 | $this->tracer->traceFunction('doctrine.dbal.transaction.rollback', function (SpanInterface $span): void { 67 | parent::rollBack(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableDriverV3.php: -------------------------------------------------------------------------------- 1 | scope(); 46 | if (null !== $scope) { 47 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 48 | } else { 49 | $this->logger?->debug('No active scope'); 50 | } 51 | $span = null; 52 | 53 | try { 54 | $spanBuilder = $this->tracer 55 | ->spanBuilder('doctrine.dbal.connection') 56 | ->setSpanKind(SpanKind::KIND_CLIENT) 57 | ->setParent($scope?->context()) 58 | ->setAttribute(TraceAttributes::DB_SYSTEM_NAME, $this->getSemanticDbSystem()) 59 | ->setAttribute(TraceAttributes::DB_NAMESPACE, $params['dbname'] ?? 'default') 60 | ; 61 | 62 | $span = $spanBuilder->startSpan(); 63 | 64 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 65 | 66 | $connection = parent::connect($params); 67 | 68 | $span->setStatus(StatusCode::STATUS_OK); 69 | 70 | return new TraceableConnection($connection, new Tracer($this->tracer, $this->logger)); 71 | } catch (Exception $exception) { 72 | $span->recordException($exception); 73 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 74 | 75 | throw $exception; 76 | } finally { 77 | if ($span instanceof SpanInterface) { 78 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 79 | $span->end(); 80 | } 81 | } 82 | } 83 | 84 | private function getSemanticDbSystem(): string 85 | { 86 | // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/db.md 87 | return match (get_class($this->getDatabasePlatform())) { 88 | AbstractMySQLPlatform::class => 'mysql', 89 | DB2Platform::class => 'db2', 90 | OraclePlatform::class => 'oracle', 91 | PostgreSQLPlatform::class => 'postgresql', 92 | SqlitePlatform::class => 'sqlite', 93 | SQLServerPlatform::class => 'mssql', 94 | default => 'other_sql', 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableDriverV4.php: -------------------------------------------------------------------------------- 1 | scope(); 48 | if (null !== $scope) { 49 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 50 | } else { 51 | $this->logger?->debug('No active scope'); 52 | } 53 | $span = null; 54 | 55 | try { 56 | $spanBuilder = $this->tracer 57 | ->spanBuilder('doctrine.dbal.connection') 58 | ->setSpanKind(SpanKind::KIND_CLIENT) 59 | ->setParent($scope?->context()) 60 | ->setAttribute(TraceAttributes::DB_NAMESPACE, $params['dbname'] ?? 'default') 61 | ->setAttribute(DoctrineTraceAttributeEnum::User->toString(), $params['user']) 62 | ; 63 | 64 | $span = $spanBuilder->startSpan(); 65 | 66 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 67 | 68 | $connection = parent::connect($params); 69 | 70 | $span->setAttribute(TraceAttributes::DB_SYSTEM_NAME, $this->getSemanticDbSystem($connection->getServerVersion())); 71 | $span->setStatus(StatusCode::STATUS_OK); 72 | 73 | return new TraceableConnection($connection, new Tracer($this->tracer, $this->logger)); 74 | } catch (Exception $exception) { 75 | $span->recordException($exception); 76 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 77 | throw $exception; 78 | } finally { 79 | if ($span instanceof SpanInterface) { 80 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 81 | $span->end(); 82 | } 83 | } 84 | } 85 | 86 | private function getSemanticDbSystem(string $serverVersion): string 87 | { 88 | // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/db.md 89 | return match (get_class($this->getDatabasePlatform(new StaticServerVersionProvider($serverVersion)))) { 90 | AbstractMySQLPlatform::class => 'mysql', 91 | DB2Platform::class => 'db2', 92 | OraclePlatform::class => 'oracle', 93 | PostgreSQLPlatform::class => 'postgresql', 94 | SQLitePlatform::class => 'sqlite', 95 | SQLServerPlatform::class => 'mssql', 96 | default => 'other_sql', 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableMiddleware.php: -------------------------------------------------------------------------------- 1 | tracer, $driver, $this->logger); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableStatementV3.php: -------------------------------------------------------------------------------- 1 | tracer->traceFunction('doctrine.dbal.statement.execute', function (SpanInterface $span) use ($params): Result { 24 | $span->setAttribute('db.params', $params); 25 | 26 | return parent::execute($params); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/TraceableStatementV4.php: -------------------------------------------------------------------------------- 1 | tracer->traceFunction('doctrine.dbal.statement.execute', function (SpanInterface $span): Result { 24 | return parent::execute(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Instrumentation/Doctrine/Middleware/Tracer.php: -------------------------------------------------------------------------------- 1 | scope(); 31 | if (null !== $scope) { 32 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 33 | } else { 34 | $this->logger?->debug('No active scope'); 35 | } 36 | $span = null; 37 | 38 | try { 39 | $spanBuilder = $this->tracer 40 | ->spanBuilder($name) 41 | ->setSpanKind(SpanKind::KIND_CLIENT) 42 | ->setParent($scope?->context()) 43 | ; 44 | 45 | $span = $spanBuilder->startSpan(); 46 | 47 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 48 | 49 | return $callback($span); 50 | } catch (Exception $exception) { 51 | if ($span instanceof SpanInterface) { 52 | $span->recordException($exception); 53 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 54 | } 55 | throw $exception; 56 | } finally { 57 | if ($span instanceof SpanInterface) { 58 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 59 | $span->end(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Instrumentation/InstrumentationTypeEnum.php: -------------------------------------------------------------------------------- 1 | tracer = new Tracer($tracer, $logger); 24 | $this->adapter = $adapter; 25 | $this->logger = $logger; 26 | } 27 | 28 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 29 | { 30 | return $this->tracer->traceFunction( 31 | 'cache.get', 32 | function (?SpanInterface $span) use ($key, $callback, $beta, $metadata): mixed { 33 | if (!$this->adapter instanceof CacheInterface) { 34 | throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); 35 | } 36 | $span?->setAttribute('cache.get', $key); 37 | 38 | return $this->adapter->get($key, $callback, $beta, $metadata); 39 | }); 40 | } 41 | 42 | public function invalidateTags(array $tags): bool 43 | { 44 | return $this->tracer->traceFunction('cache.invalidate_tags', function (?SpanInterface $span) use ($tags): bool { 45 | if (!$this->adapter instanceof TagAwareAdapterInterface) { 46 | throw new \BadMethodCallException(sprintf('The %s::invalidateTags() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, TagAwareAdapterInterface::class)); 47 | } 48 | $span?->setAttribute('cache.invalidate_tags', $tags); 49 | 50 | return $this->adapter->invalidateTags($tags); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Cache/TraceableCacheAdapter.php: -------------------------------------------------------------------------------- 1 | tracer = new Tracer($tracer, $logger); 23 | $this->adapter = $adapter; 24 | $this->logger = $logger; 25 | } 26 | 27 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 28 | { 29 | return $this->tracer->traceFunction( 30 | 'cache.get', 31 | function (?SpanInterface $span) use ($key, $callback, $beta, $metadata): mixed { 32 | if (!$this->adapter instanceof CacheInterface) { 33 | throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); 34 | } 35 | $span?->setAttribute('cache.get', $key); 36 | 37 | return $this->adapter->get($key, $callback, $beta, $metadata); 38 | } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Cache/TraceableCacheAdapterTrait.php: -------------------------------------------------------------------------------- 1 | tracer->traceFunction('cache.get_item', function (?SpanInterface $span) use ($key): CacheItem { 26 | $span?->setAttribute('cache.item.get', $key); 27 | 28 | return $this->adapter->getItem($key); 29 | }); 30 | } 31 | 32 | public function getItems(array $keys = []): iterable 33 | { 34 | return $this->tracer->traceFunction('cache.get_items', function (?SpanInterface $span) use ($keys): iterable { 35 | $span?->setAttribute('cache.items.get', $keys); 36 | 37 | return $this->adapter->getItems($keys); 38 | }); 39 | } 40 | 41 | public function clear(string $prefix = ''): bool 42 | { 43 | return $this->tracer->traceFunction('cache.clear', function (?SpanInterface $span) use ($prefix): bool { 44 | $span?->setAttribute('cache.prefix', $prefix); 45 | 46 | return $this->adapter->clear($prefix); 47 | }); 48 | } 49 | 50 | public function delete(string $key): bool 51 | { 52 | return $this->tracer->traceFunction('cache.delete', function (?SpanInterface $span) use ($key): bool { 53 | if (!$this->adapter instanceof CacheInterface) { 54 | throw new \BadMethodCallException(sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); 55 | } 56 | $span?->setAttribute('cache.delete', $key); 57 | 58 | return $this->adapter->delete($key); 59 | }); 60 | } 61 | 62 | public function hasItem(string $key): bool 63 | { 64 | return $this->tracer->traceFunction('cache.has_item', function (?SpanInterface $span) use ($key): bool { 65 | $span?->setAttribute('cache.item.has', $key); 66 | 67 | return $this->adapter->hasItem($key); 68 | }); 69 | } 70 | 71 | public function deleteItem(string $key): bool 72 | { 73 | return $this->tracer->traceFunction('cache.delete_item', function (?SpanInterface $span) use ($key): bool { 74 | $span?->setAttribute('cache.item.delete', $key); 75 | 76 | return $this->adapter->deleteItem($key); 77 | }); 78 | } 79 | 80 | public function deleteItems(array $keys): bool 81 | { 82 | return $this->tracer->traceFunction('cache.delete_items', function (?SpanInterface $span) use ($keys): bool { 83 | $span?->setAttribute('cache.items.delete', $keys); 84 | 85 | return $this->adapter->deleteItems($keys); 86 | }); 87 | } 88 | 89 | public function save(CacheItemInterface $item): bool 90 | { 91 | return $this->tracer->traceFunction('cache.save', function (?SpanInterface $span) use ($item): bool { 92 | $span?->setAttribute('cache.item.save', $item->getKey()); 93 | 94 | return $this->adapter->save($item); 95 | }); 96 | } 97 | 98 | public function saveDeferred(CacheItemInterface $item): bool 99 | { 100 | return $this->tracer->traceFunction('cache.save_deferred', function (?SpanInterface $span) use ($item): bool { 101 | $span?->setAttribute('cache.item.save_deferred', $item->getKey()); 102 | 103 | return $this->adapter->saveDeferred($item); 104 | }); 105 | } 106 | 107 | public function commit(): bool 108 | { 109 | return $this->tracer->traceFunction('cache.commit', function (?SpanInterface $span): bool { 110 | return $this->adapter->commit(); 111 | }); 112 | } 113 | 114 | public function prune(): bool 115 | { 116 | return $this->tracer->traceFunction('cache.prune', function (?SpanInterface $span): bool { 117 | if (!$this->adapter instanceof PruneableInterface) { 118 | return false; 119 | } 120 | 121 | return $this->adapter->prune(); 122 | }); 123 | } 124 | 125 | public function reset(): void 126 | { 127 | $this->tracer->traceFunction('cache.reset', function (?SpanInterface $span): void { 128 | if ($this->adapter instanceof ResettableInterface) { 129 | $this->adapter->reset(); 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Cache/Tracer.php: -------------------------------------------------------------------------------- 1 | scope(); 31 | if (null !== $scope) { 32 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 33 | } else { 34 | $this->logger?->debug('No active scope'); 35 | } 36 | $span = null; 37 | 38 | try { 39 | $spanBuilder = $this->tracer 40 | ->spanBuilder($name) 41 | ->setSpanKind(SpanKind::KIND_CLIENT) 42 | ; 43 | 44 | $span = $spanBuilder->setParent($scope?->context())->startSpan(); 45 | 46 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 47 | 48 | return $callback($span); 49 | } catch (CacheException $exception) { 50 | if ($span instanceof SpanInterface) { 51 | $span->recordException($exception); 52 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 53 | } 54 | throw $exception; 55 | } finally { 56 | if ($span instanceof SpanInterface) { 57 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 58 | $span->end(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Console/ObservableConsoleEventSubscriber.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private readonly iterable $locator, 17 | ) { 18 | } 19 | 20 | public static function getSubscribedEvents(): array 21 | { 22 | return [ 23 | ConsoleEvents::TERMINATE => [ 24 | ['flush', -10000], 25 | ], 26 | ]; 27 | } 28 | 29 | public function flush(ConsoleTerminateEvent $event): void 30 | { 31 | foreach ($this->locator as $provider) { 32 | $provider->shutdown(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Framework/Routing/TraceableRouteLoader.php: -------------------------------------------------------------------------------- 1 | loader->load($resource, $type); 24 | 25 | /** @var Route $route */ 26 | foreach ($routes as $route) { 27 | self::parseAttribute($route); 28 | 29 | $traceable = $route->getOption(self::OPTION_KEY); 30 | if (null !== $traceable) { 31 | $route->addDefaults([ 32 | self::DEFAULT_KEY => $traceable, 33 | self::TRACER_KEY => $route->getOption(self::TRACER_KEY), 34 | ]); 35 | } 36 | } 37 | 38 | return $routes; 39 | } 40 | 41 | public function supports(mixed $resource, ?string $type = null): bool 42 | { 43 | return $this->loader->supports($resource, $type); 44 | } 45 | 46 | public function getResolver(): LoaderResolverInterface 47 | { 48 | return $this->loader->getResolver(); 49 | } 50 | 51 | public function setResolver(LoaderResolverInterface $resolver): void 52 | { 53 | $this->loader->setResolver($resolver); 54 | } 55 | 56 | private static function parseAttribute(Route $route): void 57 | { 58 | try { 59 | $controller = $route->getDefault('_controller'); 60 | if (true === str_contains($controller, '::')) { 61 | $reflection = new \ReflectionMethod($controller); 62 | } else { 63 | $reflection = new \ReflectionClass($controller); 64 | } 65 | } catch (\ReflectionException) { 66 | return; 67 | } 68 | 69 | if ($reflection instanceof \ReflectionMethod) { 70 | $attribute = $reflection->getAttributes(Traceable::class)[0] ?? $reflection->getDeclaringClass()->getAttributes(Traceable::class)[0] ?? null; 71 | } else { 72 | $attribute = $reflection->getAttributes(Traceable::class)[0] ?? null; 73 | } 74 | 75 | if (null !== $attribute) { 76 | $traceable = $attribute->newInstance(); 77 | $route->addOptions([ 78 | self::OPTION_KEY => true, 79 | self::TRACER_KEY => $traceable->tracer ?? null, 80 | ]); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/HttpClient/TraceableHttpClient.php: -------------------------------------------------------------------------------- 1 | $options 29 | */ 30 | public function request(string $method, string $url, array $options = []): ResponseInterface 31 | { 32 | $scope = Context::storage()->scope(); 33 | if (null !== $scope) { 34 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 35 | } else { 36 | $this->logger?->debug('No active scope'); 37 | } 38 | 39 | $uri = new Uri($url); 40 | 41 | $spanBuilder = $this->tracer 42 | ->spanBuilder('http.client') 43 | ->setSpanKind(SpanKind::KIND_CLIENT) 44 | ->setParent($scope?->context()) 45 | ->setAttribute(TraceAttributes::URL_FULL, $url) 46 | ->setAttribute(TraceAttributes::URL_SCHEME, $uri->getScheme()) 47 | ->setAttribute(TraceAttributes::URL_PATH, $uri->getPath()) 48 | ->setAttribute(TraceAttributes::URL_QUERY, $uri->getQuery()) 49 | ->setAttribute(TraceAttributes::URL_FRAGMENT, $uri->getFragment()) 50 | ->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $method) 51 | ; 52 | 53 | $span = $spanBuilder->startSpan(); 54 | 55 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 56 | 57 | return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span); 58 | } 59 | 60 | public function stream(iterable|ResponseInterface $responses, ?float $timeout = null): ResponseStreamInterface 61 | { 62 | if ($responses instanceof TraceableResponse) { 63 | $responses = [$responses]; 64 | } elseif (!is_iterable($responses)) { 65 | throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses))); 66 | } 67 | 68 | return new ResponseStream(TraceableResponse::stream($this->client, $responses, $timeout)); 69 | } 70 | 71 | /** 72 | * @param array $options 73 | */ 74 | public function withOptions(array $options): static 75 | { 76 | $clone = clone $this; 77 | $clone->client = $this->client->withOptions($options); 78 | 79 | return $clone; 80 | } 81 | 82 | public function setLogger(LoggerInterface $logger): void 83 | { 84 | if ($this->client instanceof LoggerAwareInterface) { 85 | $this->client->setLogger($logger); 86 | } 87 | $this->logger = $logger; 88 | } 89 | 90 | public function reset(): void 91 | { 92 | if ($this->client instanceof ResetInterface) { 93 | $this->client->reset(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/HttpClient/TraceableResponse.php: -------------------------------------------------------------------------------- 1 | response, '__destruct')) { 39 | $this->response->__destruct(); 40 | } 41 | } finally { 42 | $this->endSpan(); 43 | } 44 | } 45 | 46 | public function getStatusCode(): int 47 | { 48 | return $this->response->getStatusCode(); 49 | } 50 | 51 | public function getHeaders(bool $throw = true): array 52 | { 53 | return $this->response->getHeaders($throw); 54 | } 55 | 56 | public function getContent(bool $throw = true): string 57 | { 58 | try { 59 | return $this->response->getContent($throw); 60 | } finally { 61 | $this->endSpan(); 62 | } 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function toArray(bool $throw = true): array 69 | { 70 | try { 71 | return $this->response->toArray($throw); 72 | } finally { 73 | $this->endSpan(); 74 | } 75 | } 76 | 77 | public function cancel(): void 78 | { 79 | $this->response->cancel(); 80 | $this->endSpan(); 81 | } 82 | 83 | public function getInfo(?string $type = null): mixed 84 | { 85 | return $this->response->getInfo($type); 86 | } 87 | 88 | public function toStream(bool $throw = true) 89 | { 90 | if ($throw) { 91 | $this->response->getHeaders(); 92 | } 93 | 94 | if ($this->response instanceof StreamableInterface) { 95 | return $this->response->toStream(false); 96 | } 97 | 98 | return StreamWrapper::createResource($this->response, $this->client); 99 | } 100 | 101 | /** 102 | * @internal 103 | * 104 | * @param iterable $responses 105 | * 106 | * @return \Generator 107 | */ 108 | public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator 109 | { 110 | /** @var \SplObjectStorage $traceableMap */ 111 | $traceableMap = new \SplObjectStorage(); 112 | $wrappedResponses = []; 113 | 114 | foreach ($responses as $response) { 115 | if (!$response instanceof self) { 116 | throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($response))); 117 | } 118 | 119 | $traceableMap[$response->response] = $response; 120 | $wrappedResponses[] = $response->response; 121 | } 122 | 123 | foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) { 124 | $traceableResponse = $traceableMap[$response]; 125 | $traceableResponse->endSpan(); 126 | 127 | yield $traceableResponse => $chunk; 128 | } 129 | } 130 | 131 | private function endSpan(): void 132 | { 133 | if (null === $this->span) { 134 | return; 135 | } 136 | 137 | $statusCode = $this->response->getStatusCode(); 138 | if (0 !== $statusCode && $this->span->isRecording()) { 139 | $headers = $this->response->getHeaders(false); 140 | if (isset($headers['Content-Length'])) { 141 | $this->span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $headers['Content-Length']); 142 | } 143 | 144 | $this->span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $statusCode); 145 | 146 | if ($statusCode >= 400 && $statusCode < 600) { 147 | $this->span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $statusCode); 148 | $this->span->setStatus(StatusCode::STATUS_ERROR); 149 | } 150 | } 151 | 152 | $this->logger?->debug(sprintf('Ending span "%s"', $this->span->getContext()->getSpanId())); 153 | $this->span->end(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/HttpKernel/ObservableHttpKernelEventSubscriber.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private readonly iterable $locator, 17 | ) { 18 | } 19 | 20 | public static function getSubscribedEvents(): array 21 | { 22 | return [ 23 | KernelEvents::TERMINATE => [ 24 | ['flush', 10000], 25 | ], 26 | ]; 27 | } 28 | 29 | public function flush(TerminateEvent $event): void 30 | { 31 | foreach ($this->locator as $provider) { 32 | $provider->shutdown(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Mailer/TraceableMailer.php: -------------------------------------------------------------------------------- 1 | scope(); 28 | if (null !== $scope) { 29 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 30 | } else { 31 | $this->logger?->debug('No active scope'); 32 | } 33 | $span = null; 34 | 35 | try { 36 | $spanBuilder = $this->tracer 37 | ->spanBuilder('mailer.send') 38 | ->setSpanKind(SpanKind::KIND_INTERNAL) 39 | ->setParent($scope?->context()) 40 | ; 41 | 42 | // TODO: Parse RawMessage implementations to set span attributes 43 | 44 | $span = $spanBuilder->startSpan(); 45 | 46 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 47 | 48 | $this->mailer->send($message, $envelope); 49 | } catch (TransportException $exception) { 50 | if ($span instanceof SpanInterface) { 51 | $span->recordException($exception); 52 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 53 | } 54 | throw $exception; 55 | } finally { 56 | if ($span instanceof SpanInterface) { 57 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 58 | $span->end(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Mailer/TraceableMailerTransport.php: -------------------------------------------------------------------------------- 1 | transport; 30 | } 31 | 32 | public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage 33 | { 34 | $scope = Context::storage()->scope(); 35 | if (null !== $scope) { 36 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 37 | } else { 38 | $this->logger?->debug('No active scope'); 39 | } 40 | $span = null; 41 | 42 | try { 43 | $spanBuilder = $this->tracer 44 | ->spanBuilder('mailer.transport.send') 45 | ->setSpanKind(SpanKind::KIND_CLIENT) 46 | ->setParent($scope?->context()) 47 | ; 48 | 49 | $span = $spanBuilder->startSpan(); 50 | 51 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 52 | 53 | if ($message instanceof Email) { 54 | $headers = $message->getHeaders()->addTextHeader('X-Trace', $span->getContext()->getTraceId()); 55 | $message = $message->setHeaders($headers); 56 | } 57 | 58 | return $this->transport->send( 59 | $message, 60 | $envelope, 61 | ); 62 | } catch (TransportException $exception) { 63 | if ($span instanceof SpanInterface) { 64 | $span->recordException($exception); 65 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 66 | } 67 | throw $exception; 68 | } finally { 69 | if ($span instanceof SpanInterface) { 70 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 71 | $span->end(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Messenger/TraceableMessengerMiddleware.php: -------------------------------------------------------------------------------- 1 | tracer, 25 | $stack, 26 | $this->busName, 27 | $this->eventCategory, 28 | $this->logger, 29 | ); 30 | 31 | try { 32 | return $stack->next()->handle($envelope, $stack); 33 | } finally { 34 | $stack->stop(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Messenger/TraceableMessengerStack.php: -------------------------------------------------------------------------------- 1 | scope(); 30 | if (null !== $scope) { 31 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 32 | } else { 33 | $this->logger?->debug('No active scope'); 34 | } 35 | 36 | /* if (null !== $scope) { 37 | $span = Span::fromContext($scope->context()); 38 | 39 | if ($span->isRecording()) { 40 | $scope->detach(); 41 | 42 | $span->setStatus(StatusCode::STATUS_OK); 43 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 44 | $span->end(); 45 | } 46 | }*/ 47 | 48 | $spanBuilder = $this->tracer 49 | ->spanBuilder('messenger.middleware') 50 | ->setSpanKind(SpanKind::KIND_INTERNAL) 51 | ->setParent($scope?->context()) 52 | ->setAttribute('event.category', $this->eventCategory) 53 | ->setAttribute('bus.name', $this->busName) 54 | ; 55 | 56 | $parent = Context::getCurrent(); 57 | 58 | $span = $spanBuilder->setParent($parent)->startSpan(); 59 | 60 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 61 | 62 | if ($this->stack === $nextMiddleware = $this->stack->next()) { 63 | $this->currentEvent = 'Tail'; 64 | } else { 65 | $this->currentEvent = sprintf('"%s"', get_debug_type($nextMiddleware)); 66 | } 67 | $this->currentEvent .= sprintf(' on "%s"', $this->busName); 68 | 69 | $span->setAttribute('event.current', $this->currentEvent); 70 | 71 | $context = $span->storeInContext($parent); 72 | Context::storage()->attach($context); 73 | 74 | return $nextMiddleware; 75 | } 76 | 77 | public function stop(): void 78 | { 79 | $scope = Context::storage()->scope(); 80 | if (null === $scope) { 81 | return; 82 | } 83 | 84 | $scope->detach(); 85 | 86 | $span = Span::fromContext($scope->context()); 87 | $span->setStatus(StatusCode::STATUS_OK); 88 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 89 | $span->end(); 90 | $this->currentEvent = null; 91 | } 92 | 93 | public function __clone() 94 | { 95 | $this->stack = clone $this->stack; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Messenger/TraceableMessengerTransport.php: -------------------------------------------------------------------------------- 1 | tracer = new TransportTracer($tracer, $this->logger); 21 | } 22 | 23 | public function get(): iterable 24 | { 25 | return $this->tracer->traceFunction('messenger.transport.get', function (?SpanInterface $span) { 26 | return $this->transport->get(); 27 | }); 28 | } 29 | 30 | public function ack(Envelope $envelope): void 31 | { 32 | $this->tracer->traceFunction('messenger.transport.ack', function (?SpanInterface $span) use ($envelope) { 33 | $this->transport->ack($envelope); 34 | }); 35 | } 36 | 37 | public function reject(Envelope $envelope): void 38 | { 39 | $this->tracer->traceFunction('messenger.transport.reject', function (?SpanInterface $span) use ($envelope) { 40 | $this->transport->reject($envelope); 41 | }); 42 | } 43 | 44 | public function send(Envelope $envelope): Envelope 45 | { 46 | return $this->tracer->traceFunction('messenger.transport.send', function (?SpanInterface $span) use ($envelope) { 47 | return $this->transport->send($envelope); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Messenger/TraceableMessengerTransportFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TraceableMessengerTransportFactory implements TransportFactoryInterface 17 | { 18 | public function __construct( 19 | private TransportFactory $transportFactory, 20 | private TracerInterface $tracer, 21 | private ?LoggerInterface $logger = null, 22 | ) { 23 | } 24 | 25 | /** 26 | * @param array $options 27 | */ 28 | public function createTransport(#[\SensitiveParameter] string $dsn, array $options, SerializerInterface $serializer): TransportInterface 29 | { 30 | $transport = $this->transportFactory->createTransport(Dsn::parse($dsn)->inner(), $options, $serializer); 31 | 32 | return new TraceableMessengerTransport($transport, $this->tracer, $this->logger); 33 | } 34 | 35 | /** 36 | * @param array $options 37 | */ 38 | public function supports(#[\SensitiveParameter] string $dsn, array $options): bool 39 | { 40 | $dsn = Dsn::parse($dsn); 41 | if (!$dsn instanceof Dsn\Decorated) { 42 | return false; 43 | } 44 | 45 | return $dsn->scheme()->equals('trace'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Instrumentation/Symfony/Messenger/TransportTracer.php: -------------------------------------------------------------------------------- 1 | scope(); 31 | if (null !== $scope) { 32 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 33 | } else { 34 | $this->logger?->debug('No active scope'); 35 | } 36 | $span = null; 37 | 38 | try { 39 | $spanBuilder = $this->tracer 40 | ->spanBuilder($name) 41 | ->setSpanKind(SpanKind::KIND_INTERNAL) 42 | ->setParent($scope?->context()) 43 | ; 44 | 45 | $span = $spanBuilder->startSpan(); 46 | 47 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 48 | 49 | return $callback($span); 50 | } catch (TransportException $exception) { 51 | if (null !== $span) { 52 | $span->recordException($exception); 53 | $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); 54 | } 55 | throw $exception; 56 | } finally { 57 | if ($span instanceof SpanInterface) { 58 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 59 | $span->end(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Instrumentation/Twig/TraceableTwigExtension.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private \SplObjectStorage $spans; 20 | 21 | public function __construct( 22 | private readonly TracerInterface $tracer, 23 | private readonly ?LoggerInterface $logger = null, 24 | ) { 25 | $this->spans = new \SplObjectStorage(); 26 | } 27 | 28 | public function enter(Profile $profile): void 29 | { 30 | $scope = Context::storage()->scope(); 31 | if (null !== $scope) { 32 | $this->logger?->debug(sprintf('Using scope "%s"', spl_object_id($scope))); 33 | } else { 34 | $this->logger?->debug('No active scope'); 35 | } 36 | 37 | $spanBuilder = $this->tracer 38 | ->spanBuilder($this->getSpanName($profile)) 39 | ->setSpanKind(SpanKind::KIND_INTERNAL) 40 | ->setParent($scope?->context()) 41 | ; 42 | 43 | $span = $spanBuilder->startSpan(); 44 | 45 | $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId())); 46 | 47 | $this->spans[$profile] = $span; 48 | } 49 | 50 | public function leave(Profile $profile): void 51 | { 52 | if (!isset($this->spans[$profile])) { 53 | return; 54 | } 55 | 56 | $span = $this->spans[$profile]; 57 | $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId())); 58 | $span->end(); 59 | unset($this->spans[$profile]); 60 | } 61 | 62 | public function getNodeVisitors(): array 63 | { 64 | return [ 65 | new ProfilerNodeVisitor(self::class), 66 | ]; 67 | } 68 | 69 | private function getSpanName(Profile $profile): string 70 | { 71 | switch (true) { 72 | case $profile->isRoot(): 73 | return $profile->getName(); 74 | 75 | case $profile->isTemplate(): 76 | return $profile->getTemplate(); 77 | 78 | default: 79 | return sprintf('%s::%s(%s)', $profile->getTemplate(), $profile->getType(), $profile->getName()); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Context/Attribute/ConsoleTraceAttributeEnum.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Context/Attribute/DoctrineTraceAttributeEnum.php: -------------------------------------------------------------------------------- 1 | value; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Context/Attribute/HttpKernelTraceAttributeEnum.php: -------------------------------------------------------------------------------- 1 | value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Context/Propagator/HeadersPropagator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function keys($carrier): array 16 | { 17 | return $carrier->headers->keys(); 18 | } 19 | 20 | /** 21 | * @param mixed|Request $carrier 22 | */ 23 | public function get($carrier, string $key): ?string 24 | { 25 | return count($carrier->headers->all($key)) > 1 26 | ? implode(',', $carrier->headers->all($key)) 27 | : $carrier->headers->get($key); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/ConsoleExporterEndpoint.php: -------------------------------------------------------------------------------- 1 | dsn->getExporter()) { 10 | throw new \RuntimeException('Provided DSN exporter is not compatible with this endpoint.'); 11 | } 12 | } 13 | 14 | public static function fromDsn(ExporterDsn $dsn): ExporterEndpointInterface 15 | { 16 | return new self($dsn); 17 | } 18 | 19 | public function __toString() 20 | { 21 | return $this->dsn->getPath() ?? 'php://stdout'; 22 | } 23 | 24 | public function getTransport(): ?string 25 | { 26 | return null; 27 | } 28 | 29 | public function getExporter(): string 30 | { 31 | return $this->dsn->getExporter(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/EmptyExporterOptions.php: -------------------------------------------------------------------------------- 1 | scheme()->isEmpty()) { 28 | throw new \InvalidArgumentException('The DSN must contain a scheme.'); 29 | } 30 | 31 | if (true === $parsedDsn->host()->isEmpty()) { 32 | throw new \InvalidArgumentException('The DSN must contain a host (use "default" by default).'); 33 | } 34 | 35 | return new self($parsedDsn); 36 | } 37 | 38 | public function getHost(): string 39 | { 40 | return $this->uri->host()->toString(); 41 | } 42 | 43 | public function getUser(): ?string 44 | { 45 | return $this->uri->username(); 46 | } 47 | 48 | public function getPassword(): ?string 49 | { 50 | return $this->uri->password(); 51 | } 52 | 53 | public function getPath(): ?string 54 | { 55 | return $this->uri->path()->isEmpty() ? null : $this->uri->path()->toString(); 56 | } 57 | 58 | public function getPort(?int $default = null): ?int 59 | { 60 | return $this->uri->port() ?? $default; 61 | } 62 | 63 | /** 64 | * @return string[] 65 | */ 66 | private function parseScheme(): array 67 | { 68 | return $this->uri->scheme()->segments(); 69 | } 70 | 71 | public function getExporter(): string 72 | { 73 | $parts = $this->parseScheme(); 74 | 75 | $exporter = null; 76 | if (2 === count($parts)) { 77 | $exporter = $parts[1]; 78 | } 79 | if (1 === count($parts)) { 80 | $exporter = $parts[0]; 81 | } 82 | 83 | if ('' === $exporter || null === $exporter) { 84 | throw new \InvalidArgumentException('The DSN scheme does not contain an exporter.'); 85 | } 86 | 87 | return $exporter; 88 | } 89 | 90 | public function getTransport(): ?string 91 | { 92 | $parts = $this->parseScheme(); 93 | 94 | return 2 === count($parts) ? $parts[0] : null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/ExporterEndpointInterface.php: -------------------------------------------------------------------------------- 1 | , 11 | * compression?: string, 12 | * timeout?: float, 13 | * retry?: int, 14 | * max?: int, 15 | * ca?: string, 16 | * cert?: string, 17 | * key?: string, 18 | * } 19 | */ 20 | interface ExporterOptionsInterface 21 | { 22 | /** 23 | * @param ExporterOptions $configuration 24 | */ 25 | public static function fromConfiguration(array $configuration): self; 26 | 27 | public function toTransportParams(): TransportParams; 28 | } 29 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/OtlpExporterCompressionEnum.php: -------------------------------------------------------------------------------- 1 | KnownValues::VALUE_GZIP, 16 | self::None => KnownValues::VALUE_NONE, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/OtlpExporterEndpoint.php: -------------------------------------------------------------------------------- 1 | dsn->getExporter()) { 22 | throw new \RuntimeException('Provided DSN exporter is not compatible with this endpoint.'); 23 | } 24 | $this->transport = TransportEnum::from($this->dsn->getTransport()); 25 | } 26 | 27 | public static function fromDsn(ExporterDsn $dsn): self 28 | { 29 | return new self($dsn, new HttpFactory()); 30 | } 31 | 32 | public function withSignal(string $signal): self 33 | { 34 | if (false === in_array($signal, Signals::SIGNALS, true)) { 35 | throw new \RuntimeException('Provided Otlp signal is invalid.'); 36 | } 37 | 38 | $this->signal = $signal; 39 | 40 | return $this; 41 | } 42 | 43 | public function __toString() 44 | { 45 | $uri = $this->uriFactory->createUri(); 46 | $uri = $uri 47 | ->withScheme($this->transport->getScheme()) 48 | ->withHost($this->dsn->getHost()) 49 | ->withPort($this->dsn->getPort() ?? $this->transport->getPort()) 50 | ->withPath($this->dsn->getPath() ?? $this->getPath()); 51 | 52 | if (null !== $this->dsn->getUser()) { 53 | $uri = $uri->withUserInfo($this->dsn->getUser(), $this->dsn->getPassword()); 54 | } 55 | 56 | return (string) $uri; 57 | } 58 | 59 | private function getPath(): string 60 | { 61 | if (null === $this->signal) { 62 | throw new \RuntimeException('Signal for Otlp endpoint was not provided'); 63 | } 64 | 65 | return match ($this->transport) { 66 | TransportEnum::Grpc, TransportEnum::Grpcs => OtlpUtil::method($this->signal), 67 | TransportEnum::Http, TransportEnum::Https => HttpEndpointResolverInterface::DEFAULT_PATHS[$this->signal], 68 | default => '', 69 | }; 70 | } 71 | 72 | public function getTransport(): ?string 73 | { 74 | return $this->transport->value; 75 | } 76 | 77 | public function getExporter(): string 78 | { 79 | return 'otlp'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/OtlpExporterFormatEnum.php: -------------------------------------------------------------------------------- 1 | ContentTypes::JSON, 18 | self::Ndjson => ContentTypes::NDJSON, 19 | self::Grpc, self::Protobuf => ContentTypes::PROTOBUF, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Exporter/OtlpExporterOptions.php: -------------------------------------------------------------------------------- 1 | $headers 21 | */ 22 | public function __construct( 23 | private OtlpExporterFormatEnum $format = OtlpExporterFormatEnum::Json, 24 | private array $headers = [], 25 | private OtlpExporterCompressionEnum $compression = OtlpExporterCompressionEnum::None, 26 | private float $timeout = self::DEFAULT_TIMEOUT, 27 | private int $retryDelay = self::DEFAULT_RETRY_DELAY, 28 | private int $maxRetries = self::DEFAULT_MAX_RETRIES, 29 | private ?string $caCertificate = null, 30 | private ?string $certificate = null, 31 | private ?string $key = null, 32 | ) { 33 | } 34 | 35 | public static function fromConfiguration(array $configuration): self 36 | { 37 | $options = new self(); 38 | 39 | if (isset($configuration['format']) && null !== OtlpExporterFormatEnum::tryFrom($configuration['format'])) { 40 | $options->format = OtlpExporterFormatEnum::from($configuration['format']); 41 | } 42 | 43 | if (isset($configuration['headers'])) { 44 | $options->headers = $configuration['headers']; 45 | } 46 | 47 | $otlpHeaders = OtlpUtil::getUserAgentHeader(); 48 | $otlpHeaders['User-Agent'] .= ', Symfony OTEL Bundle'; 49 | $options->headers += $otlpHeaders; 50 | 51 | if (isset($configuration['compression']) && null !== OtlpExporterCompressionEnum::tryFrom($configuration['compression'])) { 52 | $options->compression = OtlpExporterCompressionEnum::from($configuration['compression']); 53 | } 54 | 55 | if (isset($configuration['timeout'])) { 56 | $options->timeout = $configuration['timeout']; 57 | } 58 | 59 | if (isset($configuration['retry'])) { 60 | $options->retryDelay = $configuration['retry']; 61 | } 62 | 63 | if (isset($configuration['max'])) { 64 | $options->maxRetries = $configuration['max']; 65 | } 66 | 67 | if (isset($configuration['ca'])) { 68 | $options->caCertificate = $configuration['ca']; 69 | } 70 | 71 | if (isset($configuration['cert'])) { 72 | $options->certificate = $configuration['cert']; 73 | } 74 | 75 | if (isset($configuration['key'])) { 76 | $options->key = $configuration['key']; 77 | } 78 | 79 | return $options; 80 | } 81 | 82 | public function toTransportParams(): TransportParams 83 | { 84 | return new TransportParams( 85 | $this->getFormat()->toContentType(), 86 | $this->getHeaders(), 87 | $this->getCompression()->toKnownValue(), 88 | $this->getTimeout(), 89 | $this->getRetryDelay(), 90 | $this->getMaxRetries(), 91 | $this->getCaCertificate(), 92 | $this->getCertificate(), 93 | $this->getKey(), 94 | ); 95 | } 96 | 97 | public function getFormat(): OtlpExporterFormatEnum 98 | { 99 | return $this->format; 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function getHeaders(): array 106 | { 107 | $otlpHeaders = OtlpUtil::getUserAgentHeader(); 108 | $otlpHeaders['User-Agent'] .= ', Symfony OTEL Bundle'; 109 | 110 | return $this->headers + $otlpHeaders; 111 | } 112 | 113 | public function getCompression(): OtlpExporterCompressionEnum 114 | { 115 | return $this->compression; 116 | } 117 | 118 | public function getTimeout(): float 119 | { 120 | return $this->timeout; 121 | } 122 | 123 | public function getRetryDelay(): int 124 | { 125 | return $this->retryDelay; 126 | } 127 | 128 | public function getMaxRetries(): int 129 | { 130 | return $this->maxRetries; 131 | } 132 | 133 | public function getCaCertificate(): ?string 134 | { 135 | return $this->caCertificate; 136 | } 137 | 138 | public function getCertificate(): ?string 139 | { 140 | return $this->certificate; 141 | } 142 | 143 | public function getKey(): ?string 144 | { 145 | return $this->key; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/AbstractLogExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 15 | } 16 | 17 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): ConsoleExporter 18 | { 19 | return new ConsoleExporter($this->transportFactory->createTransport(LogExporterEndpoint::fromDsn($dsn), $options)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/InMemoryLogExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 14 | } 15 | 16 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): InMemoryExporter 17 | { 18 | return new InMemoryExporter(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/LogExporterEnum.php: -------------------------------------------------------------------------------- 1 | getExporter()); 17 | 18 | if (null === $exporter) { 19 | throw new \InvalidArgumentException('Unsupported DSN for Log exporter.'); 20 | } 21 | 22 | return $exporter; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/LogExporterFactory.php: -------------------------------------------------------------------------------- 1 | $factories 13 | */ 14 | public function __construct(private readonly iterable $factories) 15 | { 16 | } 17 | 18 | public function supports(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): bool 19 | { 20 | foreach ($this->factories as $factory) { 21 | if ($factory->supports($dsn, $options)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): LogRecordExporterInterface 30 | { 31 | foreach ($this->factories as $factory) { 32 | if ($factory->supports($dsn, $options)) { 33 | return $factory->createExporter($dsn, $options); 34 | } 35 | } 36 | 37 | throw new \InvalidArgumentException('No Log exporter supports the given DSN.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/LogExporterFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface LogExporterFactoryInterface extends ExporterFactoryInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/NoopLogExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 14 | } 15 | 16 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): NoopExporter 17 | { 18 | return new NoopExporter(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporter/OtlpLogExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 15 | } 16 | 17 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): LogsExporter 18 | { 19 | return new LogsExporter($this->transportFactory->createTransport(LogExporterEndpoint::fromDsn($dsn), $options)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogExporterEndpoint.php: -------------------------------------------------------------------------------- 1 | exporter = LogExporterEnum::fromDsn($dsn); 23 | $this->transport = TransportEnum::fromDsn($dsn); 24 | } 25 | 26 | public static function fromDsn(ExporterDsn $dsn): self 27 | { 28 | return new self($dsn); 29 | } 30 | 31 | public function __toString() 32 | { 33 | if (LogExporterEnum::Console === $this->exporter) { 34 | return (string) ConsoleExporterEndpoint::fromDsn($this->dsn); 35 | } 36 | 37 | if (LogExporterEnum::Otlp === $this->exporter) { 38 | return (string) OtlpExporterEndpoint::fromDsn($this->dsn)->withSignal(Signals::LOGS); 39 | } 40 | 41 | return ''; 42 | } 43 | 44 | public function getTransport(): ?string 45 | { 46 | return $this->transport?->value; 47 | } 48 | 49 | public function getExporter(): string 50 | { 51 | return $this->exporter->value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogProcessor/AbstractLogProcessorFactory.php: -------------------------------------------------------------------------------- 1 | = count($processors)) { 17 | throw new \InvalidArgumentException('You must provide at least one processor when using a multi log processor'); 18 | } 19 | 20 | return new MultiLogRecordProcessor($processors); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LogProcessor/NoopLogProcessorFactory.php: -------------------------------------------------------------------------------- 1 | build()->getAttributeFactory()); 17 | 18 | assert($processor instanceof LogRecordProcessorInterface); 19 | 20 | return new LoggerProvider($processor, $instrumentationScopeFactory, $resource); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Log/LoggerProvider/LoggerProviderEnum.php: -------------------------------------------------------------------------------- 1 | new AllExemplarFilter(), 25 | ExemplarFilterEnum::None => new NoneExemplarFilter(), 26 | ExemplarFilterEnum::WithSampledTrace => new WithSampledTraceExemplarFilter(), 27 | ExemplarFilterEnum::Service => $params['service_id'], 28 | default => throw new \InvalidArgumentException(sprintf('Unknown exemplar filter: %s', $name)), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MeterProvider/AbstractMeterProviderFactory.php: -------------------------------------------------------------------------------- 1 | setResource($resource ?? ResourceInfoFactory::defaultResource()) 21 | ->addReader($reader) 22 | ->setExemplarFilter($filter) 23 | ->build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MeterProvider/MeterProviderEnum.php: -------------------------------------------------------------------------------- 1 | getExporter()); 19 | } 20 | 21 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): ConsoleMetricExporter 22 | { 23 | assert($options instanceof MetricExporterOptions); 24 | 25 | return new ConsoleMetricExporter($options->getTemporality()->toData()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/InMemoryMetricExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 19 | } 20 | 21 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): InMemoryExporter 22 | { 23 | assert($options instanceof MetricExporterOptions); 24 | 25 | return new InMemoryExporter(temporality: $options->getTemporality()->toData()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/MetricExporterEnum.php: -------------------------------------------------------------------------------- 1 | getExporter()); 18 | 19 | if (null === $exporter) { 20 | throw new \InvalidArgumentException('Unsupported DSN for Metric exporter'); 21 | } 22 | 23 | return self::from($dsn->getExporter()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/MetricExporterFactory.php: -------------------------------------------------------------------------------- 1 | $factories 13 | */ 14 | public function __construct(private readonly iterable $factories) 15 | { 16 | } 17 | 18 | public function supports(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): bool 19 | { 20 | foreach ($this->factories as $factory) { 21 | if ($factory->supports($dsn, $options)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): MetricExporterInterface 30 | { 31 | foreach ($this->factories as $factory) { 32 | if ($factory->supports($dsn, $options)) { 33 | return $factory->createExporter($dsn, $options); 34 | } 35 | } 36 | 37 | throw new \InvalidArgumentException('No Metric exporter factory supports the given DSN.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/MetricExporterFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface MetricExporterFactoryInterface extends ExporterFactoryInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/MetricTemporalityEnum.php: -------------------------------------------------------------------------------- 1 | Temporality::DELTA, 18 | self::Cumulative => Temporality::CUMULATIVE, 19 | self::LowMemory => 'LowMemory', 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/NoopMetricExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 14 | } 15 | 16 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): NoopMetricExporter 17 | { 18 | return new NoopMetricExporter(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporter/OtlpMetricExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 20 | } 21 | 22 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): MetricExporter 23 | { 24 | assert($options instanceof MetricExporterOptions); 25 | 26 | return new MetricExporter($this->transportFactory->createTransport( 27 | MetricExporterEndpoint::fromDsn($dsn), 28 | $options->getOtlpOptions(), 29 | ), $options->getTemporality()->toData()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporterEndpoint.php: -------------------------------------------------------------------------------- 1 | exporter = MetricExporterEnum::fromDsn($this->dsn); 21 | $this->transport = TransportEnum::fromDsn($this->dsn); 22 | } 23 | 24 | public static function fromDsn(ExporterDsn $dsn): self 25 | { 26 | return new self($dsn); 27 | } 28 | 29 | public function __toString() 30 | { 31 | if (MetricExporterEnum::Console === $this->exporter) { 32 | return (string) ConsoleExporterEndpoint::fromDsn($this->dsn); 33 | } 34 | 35 | if (MetricExporterEnum::Otlp === $this->exporter) { 36 | return (string) OtlpExporterEndpoint::fromDsn($this->dsn)->withSignal(Signals::METRICS); 37 | } 38 | 39 | return ''; 40 | } 41 | 42 | public function getTransport(): ?string 43 | { 44 | return $this->transport?->value; 45 | } 46 | 47 | public function getExporter(): string 48 | { 49 | return $this->exporter->value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Metric/MetricExporterOptions.php: -------------------------------------------------------------------------------- 1 | temporality = MetricTemporalityEnum::from($configuration['temporality']); 32 | } 33 | 34 | $options->otlpOptions = OtlpExporterOptions::fromConfiguration($configuration); 35 | 36 | return $options; 37 | } 38 | 39 | public function getTemporality(): MetricTemporalityEnum 40 | { 41 | return $this->temporality; 42 | } 43 | 44 | public function getOtlpOptions(): ?OtlpExporterOptions 45 | { 46 | return $this->otlpOptions; 47 | } 48 | 49 | public function toTransportParams(): TransportParams 50 | { 51 | return $this->otlpOptions->toTransportParams(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Resource/ResourceInfoFactory.php: -------------------------------------------------------------------------------- 1 | merge(ResourceInfo::create(Attributes::create([ 16 | ResourceAttributes::SERVICE_NAMESPACE => $namespace, 17 | ResourceAttributes::SERVICE_NAME => $name, 18 | ResourceAttributes::SERVICE_VERSION => $version, 19 | ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => $environment, 20 | ]), ResourceAttributes::SCHEMA_URL)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/Sampler/AttributeBasedSampler.php: -------------------------------------------------------------------------------- 1 | $attributes 21 | */ 22 | public function shouldSample( 23 | ContextInterface $parentContext, 24 | string $traceId, 25 | string $spanName, 26 | int $spanKind, 27 | AttributesInterface $attributes, 28 | array $links, 29 | ): SamplingResult { 30 | $parentSpan = Span::fromContext($parentContext); 31 | $parentSpanContext = $parentSpan->getContext(); 32 | $traceState = $parentSpanContext->getTraceState(); 33 | 34 | if ($attributes->has($this->attributeName)) { 35 | $attributeValue = $attributes->get($this->attributeName); 36 | if (null !== $attributeValue && $attributeValue !== $this->attributeValue) { 37 | return new SamplingResult(SamplingResult::DROP, [], $traceState); 38 | } 39 | 40 | return new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], $traceState); 41 | } 42 | 43 | return new SamplingResult(SamplingResult::DROP, [], $traceState); 44 | } 45 | 46 | public function getDescription(): string 47 | { 48 | return sprintf('AttributeBasedSampler{%s, %s}', $this->attributeName, $this->attributeValue); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SamplerFactory.php: -------------------------------------------------------------------------------- 1 | $params 16 | */ 17 | public static function create(string $name, array $params = []): SamplerInterface 18 | { 19 | $sampler = TraceSamplerEnum::tryFrom($name); 20 | 21 | if (isset($params['service_id']) && false === $params['service_id'] instanceof SamplerInterface) { 22 | throw new \InvalidArgumentException('Parameter service_id must be an instance of SamplerInterface'); 23 | } 24 | 25 | return match ($sampler) { 26 | TraceSamplerEnum::AlwaysOn => new AlwaysOnSampler(), 27 | TraceSamplerEnum::AlwaysOff => new AlwaysOffSampler(), 28 | TraceSamplerEnum::ParentBasedAlwaysOn => new ParentBased(new AlwaysOnSampler()), 29 | TraceSamplerEnum::ParentBasedAlwaysOff => new ParentBased(new AlwaysOffSampler()), 30 | TraceSamplerEnum::ParentBasedTraceIdRatio => new ParentBased(new TraceIdRatioBasedSampler(...$params)), 31 | TraceSamplerEnum::TraceIdRatio => new TraceIdRatioBasedSampler(...$params), 32 | TraceSamplerEnum::AttributeBased => new AttributeBasedSampler(...$params), 33 | TraceSamplerEnum::Service => $params['service_id'], 34 | default => throw new \InvalidArgumentException(sprintf('Unknown sampler: %s', $name)), 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/AbstractSpanExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 15 | } 16 | 17 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): ConsoleSpanExporter 18 | { 19 | return new ConsoleSpanExporter($this->transportFactory->createTransport(TraceExporterEndpoint::fromDsn($dsn), $options)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/InMemorySpanExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 14 | } 15 | 16 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): InMemoryExporter 17 | { 18 | return new InMemoryExporter(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/OtlpSpanExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 15 | } 16 | 17 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): SpanExporter 18 | { 19 | return new SpanExporter($this->transportFactory->createTransport( 20 | TraceExporterEndpoint::fromDsn($dsn), 21 | $options, 22 | )); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/SpanExporterFactory.php: -------------------------------------------------------------------------------- 1 | $factories 13 | */ 14 | public function __construct(private readonly iterable $factories) 15 | { 16 | } 17 | 18 | public function supports(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): bool 19 | { 20 | foreach ($this->factories as $factory) { 21 | if ($factory->supports($dsn, $options)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): SpanExporterInterface 30 | { 31 | foreach ($this->factories as $factory) { 32 | if ($factory->supports($dsn, $options)) { 33 | return $factory->createExporter($dsn, $options); 34 | } 35 | } 36 | 37 | throw new \InvalidArgumentException('No span exporter supports the given DSN.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/SpanExporterFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface SpanExporterFactoryInterface extends ExporterFactoryInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/TraceExporterEnum.php: -------------------------------------------------------------------------------- 1 | getExporter()); 17 | 18 | if (null === $exporter) { 19 | throw new \InvalidArgumentException('Unsupported DSN for Trace exporter'); 20 | } 21 | 22 | return $exporter; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanExporter/ZipkinSpanExporterFactory.php: -------------------------------------------------------------------------------- 1 | getExporter()); 15 | } 16 | 17 | public function createExporter(#[\SensitiveParameter] ExporterDsn $dsn, ExporterOptionsInterface $options): Exporter 18 | { 19 | return new Exporter($this->transportFactory->createTransport( 20 | TraceExporterEndpoint::fromDsn($dsn), 21 | $options, 22 | )); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanProcessor/AbstractSpanProcessorFactory.php: -------------------------------------------------------------------------------- 1 | = count($processors)) { 16 | throw new \InvalidArgumentException('Processors should not be empty'); 17 | } 18 | 19 | return new MultiSpanProcessor(...$processors); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/SpanProcessor/NoopSpanProcessorFactory.php: -------------------------------------------------------------------------------- 1 | exporter = TraceExporterEnum::fromDsn($this->dsn); 21 | $this->transport = TransportEnum::fromDsn($this->dsn); 22 | } 23 | 24 | public static function fromDsn(ExporterDsn $dsn): self 25 | { 26 | return new self($dsn); 27 | } 28 | 29 | public function __toString() 30 | { 31 | if (TraceExporterEnum::Console === $this->exporter) { 32 | return (string) ConsoleExporterEndpoint::fromDsn($this->dsn); 33 | } 34 | 35 | if (TraceExporterEnum::Zipkin === $this->exporter) { 36 | return (string) ZipkinExporterEndpoint::fromDsn($this->dsn); 37 | } 38 | 39 | if (TraceExporterEnum::Otlp === $this->exporter) { 40 | return (string) OtlpExporterEndpoint::fromDsn($this->dsn)->withSignal(Signals::TRACE); 41 | } 42 | 43 | return ''; 44 | } 45 | 46 | public function getExporter(): string 47 | { 48 | return $this->exporter->value; 49 | } 50 | 51 | public function getTransport(): ?string 52 | { 53 | return $this->transport?->value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/TraceSamplerEnum.php: -------------------------------------------------------------------------------- 1 | = count($processors)) { 15 | throw new \InvalidArgumentException('Processors should not be empty'); 16 | } 17 | 18 | return new TracerProvider($processors, $sampler, $info); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Trace/TracerProvider/NoopTracerProviderFactory.php: -------------------------------------------------------------------------------- 1 | dsn)) { 21 | throw new \InvalidArgumentException('Unsupported DSN exporter for this endpoint.'); 22 | } 23 | $this->transport = TransportEnum::fromDsn($this->dsn); 24 | } 25 | 26 | public static function fromDsn(ExporterDsn $dsn): self 27 | { 28 | return new self($dsn, new HttpFactory()); 29 | } 30 | 31 | public function __toString() 32 | { 33 | $uri = $this->uriFactory->createUri(); 34 | $uri = $uri 35 | ->withScheme($this->transport->getScheme()) 36 | ->withHost($this->dsn->getHost()) 37 | ->withPort($this->dsn->getPort() ?? 9411) 38 | ->withPath($this->dsn->getPath() ?? '/api/v2/spans'); 39 | 40 | if (null !== $this->dsn->getUser()) { 41 | $uri = $uri->withUserInfo($this->dsn->getUser(), $this->dsn->getPassword()); 42 | } 43 | 44 | return (string) $uri; 45 | } 46 | 47 | public function getTransport(): ?string 48 | { 49 | return $this->transport->value; 50 | } 51 | 52 | public function getExporter(): string 53 | { 54 | return 'zipkin'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/AbstractTransportFactory.php: -------------------------------------------------------------------------------- 1 | getTransport() 16 | && in_array(TransportEnum::tryFrom($endpoint->getTransport()), [TransportEnum::Grpc, TransportEnum::Grpcs], true); 17 | } 18 | 19 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface 20 | { 21 | $params = $options->toTransportParams(); 22 | 23 | $compression = OtlpExporterCompressionEnum::tryFrom($params->compression) ?? OtlpExporterCompressionEnum::None; 24 | 25 | return (new \OpenTelemetry\Contrib\Grpc\GrpcTransportFactory())->create( 26 | (string) $endpoint, 27 | OtlpExporterFormatEnum::Grpc->toContentType(), 28 | $params->headers, 29 | $compression->toKnownValue(), 30 | $params->timeout, 31 | $params->retryDelay, 32 | $params->maxRetries, 33 | $params->caCert, 34 | $params->cert, 35 | $params->key, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/OtlpHttpTransportFactory.php: -------------------------------------------------------------------------------- 1 | getTransport()) { 20 | return false; 21 | } 22 | 23 | if ('otlp' !== $endpoint->getExporter()) { 24 | return false; 25 | } 26 | 27 | return in_array(TransportEnum::tryFrom($endpoint->getTransport()), [TransportEnum::Http, TransportEnum::Https], true); 28 | } 29 | 30 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface 31 | { 32 | $params = $options->toTransportParams(); 33 | $format = $params->contentType ?? OtlpExporterFormatEnum::Json->toContentType(); 34 | $compression = OtlpExporterCompressionEnum::tryFrom($params->compression) ?? OtlpExporterCompressionEnum::None; 35 | 36 | return new PsrTransport( 37 | new Client(), 38 | new HttpFactory(), 39 | new HttpFactory(), 40 | (string) $endpoint, 41 | $format, 42 | $params->headers, 43 | PsrUtils::compression(OtlpExporterCompressionEnum::None === $compression ? null : $compression->toKnownValue()), 44 | $params->retryDelay, 45 | $params->maxRetries, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/PsrHttpTransportFactory.php: -------------------------------------------------------------------------------- 1 | getTransport() 20 | && in_array(TransportEnum::tryFrom($endpoint->getTransport()), [TransportEnum::Http, TransportEnum::Https], true); 21 | } 22 | 23 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface 24 | { 25 | $params = $options->toTransportParams(); 26 | $format = $params->contentType ?? OtlpExporterFormatEnum::Json->toContentType(); 27 | $compression = OtlpExporterCompressionEnum::tryFrom($params->compression) ?? OtlpExporterCompressionEnum::None; 28 | 29 | return new PsrTransport( 30 | new Client(), 31 | new HttpFactory(), 32 | new HttpFactory(), 33 | (string) $endpoint, 34 | $format, 35 | $params->headers, 36 | PsrUtils::compression($compression->toKnownValue()), 37 | $params->retryDelay, 38 | $params->maxRetries, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/StreamTransportFactory.php: -------------------------------------------------------------------------------- 1 | getTransport() 16 | && TransportEnum::Stream === TransportEnum::tryFrom($endpoint->getTransport()); 17 | } 18 | 19 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface 20 | { 21 | $params = $options->toTransportParams(); 22 | $format = $params->contentType ?? OtlpExporterFormatEnum::Json->toContentType(); 23 | $compression = OtlpExporterCompressionEnum::tryFrom($params->compression) ?? OtlpExporterCompressionEnum::None; 24 | 25 | return (new \OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory())->create( 26 | (string) $endpoint, 27 | $format, 28 | $params->headers, 29 | $compression->toKnownValue(), 30 | $params->timeout, 31 | $params->retryDelay, 32 | $params->maxRetries, 33 | $params->caCert, 34 | $params->cert, 35 | $params->key, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/TransportEnum.php: -------------------------------------------------------------------------------- 1 | 'http', 19 | self::Https, self::Grpcs => 'https', 20 | default => null, 21 | }; 22 | } 23 | 24 | public function getPort(): ?int 25 | { 26 | return match ($this) { 27 | self::Grpc, self::Grpcs => 4317, 28 | self::Http, self::Https => 4318, 29 | default => null, 30 | }; 31 | } 32 | 33 | public static function fromDsn(ExporterDsn $dsn): ?self 34 | { 35 | return TransportEnum::tryFrom($dsn->getTransport() ?? ''); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/TransportFactory.php: -------------------------------------------------------------------------------- 1 | $factories 13 | */ 14 | public function __construct(private readonly iterable $factories) 15 | { 16 | } 17 | 18 | public function supports(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): bool 19 | { 20 | foreach ($this->factories as $factory) { 21 | if ($factory->supports($endpoint, $options)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface 30 | { 31 | foreach ($this->factories as $factory) { 32 | if ($factory->supports($endpoint, $options)) { 33 | return $factory->createTransport($endpoint, $options); 34 | } 35 | } 36 | 37 | throw new \InvalidArgumentException('No transport supports the given endpoint.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/TransportFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function createTransport(#[\SensitiveParameter] ExporterEndpointInterface $endpoint, ExporterOptionsInterface $options): TransportInterface; 17 | } 18 | -------------------------------------------------------------------------------- /src/OpenTelemetry/Transport/TransportParams.php: -------------------------------------------------------------------------------- 1 | $headers 9 | */ 10 | public function __construct( 11 | public ?string $contentType = 'application/json', 12 | public array $headers = [], 13 | public ?string $compression = 'none', 14 | public float $timeout = .10, 15 | public int $retryDelay = 100, 16 | public int $maxRetries = 3, 17 | public ?string $caCert = null, 18 | public ?string $cert = null, 19 | public ?string $key = null, 20 | ) { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTelemetryBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new SetInstrumentationTypePass()); 32 | 33 | $container->addCompilerPass(new CacheInstrumentationPass()); 34 | $container->addCompilerPass(new HttpClientInstrumentationPass()); 35 | $container->addCompilerPass(new SetHttpKernelTracingExcludePathsPass()); 36 | $container->addCompilerPass(new SetConsoleTracingExcludeCommandsPass()); 37 | 38 | $container->addCompilerPass(new TracerLocatorPass()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | parameters() 19 | ->set('open_telemetry.bundle.name', OpenTelemetryBundle::name()) 20 | ->set('open_telemetry.bundle.version', OpenTelemetryBundle::version()) 21 | ->set('monolog.additional_channels', ['open_telemetry']) 22 | ; 23 | 24 | $container->services() 25 | ->defaults() 26 | ->private() 27 | 28 | ->set('open_telemetry.resource_info', ResourceInfo::class) 29 | ->factory([ResourceInfoFactory::class, 'create']) 30 | 31 | ->set('open_telemetry.propagator.server_timing', ServerTimingPropagator::class) 32 | ->set('open_telemetry.propagator.trace_response', TraceResponsePropagator::class) 33 | 34 | ->set('open_telemetry.propagator_text_map.noop', NoopTextMapPropagator::class) 35 | ->set('open_telemetry.propagator_text_map.multi', MultiTextMapPropagator::class) 36 | 37 | ->set('open_telemetry.propagation_getter.headers', HeadersPropagationGetter::class) 38 | ->set('open_telemetry.propagation_getter.sanitize_combined_headers', SanitizeCombinedHeadersPropagationGetter::class) 39 | 40 | ->set('open_telemetry.propagation_getter_setter.array_access', ArrayAccessGetterSetter::class) 41 | 42 | ->set('open_telemetry.exporter_dsn', ExporterDsn::class) 43 | ->factory([ExporterDsn::class, 'fromString']) 44 | 45 | ->set('open_telemetry.otlp_exporter_options', OtlpExporterOptions::class) 46 | ->factory([OtlpExporterOptions::class, 'fromConfiguration']) 47 | ; 48 | }; 49 | -------------------------------------------------------------------------------- /src/Resources/config/services_metering_instrumentation.php: -------------------------------------------------------------------------------- 1 | services() 11 | ->defaults() 12 | ->private() 13 | 14 | // Console 15 | ->set('open_telemetry.instrumentation.console.metric.event_subscriber', ObservableConsoleEventSubscriber::class) 16 | ->args([tagged_iterator('open_telemetry.metrics.provider')]) 17 | ->tag('kernel.event_subscriber') 18 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 19 | 20 | // HTTP Kernel 21 | ->set('open_telemetry.instrumentation.http_kernel.metric.event_subscriber', ObservableHttpKernelEventSubscriber::class) 22 | ->args([tagged_iterator('open_telemetry.metrics.provider')]) 23 | ->tag('kernel.event_subscriber') 24 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 25 | 26 | ; 27 | }; 28 | -------------------------------------------------------------------------------- /src/Resources/config/services_metrics.php: -------------------------------------------------------------------------------- 1 | services() 24 | ->defaults() 25 | ->private() 26 | 27 | ->set('open_telemetry.metric_exporter_options', MetricExporterOptions::class) 28 | ->factory([MetricExporterOptions::class, 'fromConfiguration']) 29 | 30 | // Exemplar Filters 31 | ->set('open_telemetry.metrics.exemplar_filter_factory', ExemplarFilterFactory::class) 32 | ->factory([ExemplarFilterFactory::class, 'create']) 33 | 34 | // Exporters 35 | ->set('open_telemetry.metrics.exporter_factory.abstract', AbstractMetricExporterFactory::class) 36 | ->abstract() 37 | ->args([ 38 | service('open_telemetry.transport_factory'), 39 | service('logger')->ignoreOnInvalid(), 40 | ]) 41 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 42 | 43 | ->set('open_telemetry.metrics.exporter_factory.console', ConsoleMetricExporterFactory::class) 44 | ->parent('open_telemetry.metrics.exporter_factory.abstract') 45 | ->tag('open_telemetry.metrics.exporter_factory') 46 | 47 | ->set('open_telemetry.metrics.exporter_factory.in-memory', InMemoryMetricExporterFactory::class) 48 | ->parent('open_telemetry.metrics.exporter_factory.abstract') 49 | ->tag('open_telemetry.metrics.exporter_factory') 50 | 51 | ->set('open_telemetry.metrics.exporter_factory.noop', NoopMetricExporterFactory::class) 52 | ->parent('open_telemetry.metrics.exporter_factory.abstract') 53 | ->tag('open_telemetry.metrics.exporter_factory') 54 | 55 | ->set('open_telemetry.metrics.exporter_factory.otlp', OtlpMetricExporterFactory::class) 56 | ->parent('open_telemetry.metrics.exporter_factory.abstract') 57 | ->tag('open_telemetry.metrics.exporter_factory') 58 | 59 | ->set('open_telemetry.metrics.exporter_factory', MetricExporterFactory::class) 60 | ->args([ 61 | tagged_iterator('open_telemetry.metrics.exporter_factory'), 62 | ]) 63 | 64 | ->set('open_telemetry.metrics.exporter_interface', MetricExporterInterface::class) 65 | ->factory([service('open_telemetry.metrics.exporter_factory'), 'createExporter']) 66 | 67 | // Providers 68 | ->set('open_telemetry.metrics.provider_factory.abstract', AbstractMeterProviderFactory::class) 69 | ->abstract() 70 | ->args([ 71 | service('logger')->ignoreOnInvalid(), 72 | ]) 73 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 74 | 75 | ->set('open_telemetry.metrics.provider_factory.noop', NoopMeterProviderFactory::class) 76 | ->parent('open_telemetry.metrics.provider_factory.abstract') 77 | 78 | ->set('open_telemetry.metrics.provider_factory.default', DefaultMeterProviderFactory::class) 79 | ->parent('open_telemetry.metrics.provider_factory.abstract') 80 | 81 | ->set('open_telemetry.metrics.provider_interface', MeterProviderInterface::class) 82 | 83 | // Meter 84 | ->set('open_telemetry.metrics.meter_interface', MeterInterface::class) 85 | ; 86 | }; 87 | -------------------------------------------------------------------------------- /src/Resources/config/services_traces.php: -------------------------------------------------------------------------------- 1 | services() 28 | ->defaults() 29 | ->private() 30 | 31 | ->set('open_telemetry.traces.sampler_factory', SamplerFactory::class) 32 | ->factory([SamplerFactory::class, 'create']) 33 | 34 | // Exporters 35 | ->set('open_telemetry.traces.exporter_factory.abstract', AbstractSpanExporterFactory::class) 36 | ->abstract() 37 | ->args([ 38 | service('open_telemetry.transport_factory'), 39 | service('logger')->ignoreOnInvalid(), 40 | ]) 41 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 42 | 43 | ->set('open_telemetry.traces.exporter_factory.console', ConsoleSpanExporterFactory::class) 44 | ->parent('open_telemetry.traces.exporter_factory.abstract') 45 | ->tag('open_telemetry.traces.exporter_factory') 46 | 47 | ->set('open_telemetry.traces.exporter_factory.in-memory', InMemorySpanExporterFactory::class) 48 | ->parent('open_telemetry.traces.exporter_factory.abstract') 49 | ->tag('open_telemetry.traces.exporter_factory') 50 | 51 | ->set('open_telemetry.traces.exporter_factory.otlp', OtlpSpanExporterFactory::class) 52 | ->parent('open_telemetry.traces.exporter_factory.abstract') 53 | ->tag('open_telemetry.traces.exporter_factory') 54 | 55 | ->set('open_telemetry.traces.exporter_factory.zipkin', ZipkinSpanExporterFactory::class) 56 | ->parent('open_telemetry.traces.exporter_factory.abstract') 57 | ->tag('open_telemetry.traces.exporter_factory') 58 | 59 | ->set('open_telemetry.traces.exporter_factory', SpanExporterFactory::class) 60 | ->args([ 61 | tagged_iterator('open_telemetry.traces.exporter_factory'), 62 | ]) 63 | 64 | ->set('open_telemetry.traces.exporter_interface', SpanExporterInterface::class) 65 | ->factory([service('open_telemetry.traces.exporter_factory'), 'createExporter']) 66 | 67 | // Processors 68 | ->set('open_telemetry.traces.processor_factory.abstract', AbstractSpanProcessorFactory::class) 69 | ->abstract() 70 | ->args([ 71 | service('logger')->ignoreOnInvalid(), 72 | ]) 73 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 74 | 75 | ->set('open_telemetry.traces.processor_factory.multi', MultiSpanProcessorFactory::class) 76 | ->parent('open_telemetry.traces.processor_factory.abstract') 77 | 78 | ->set('open_telemetry.traces.processor_factory.noop', NoopSpanProcessorFactory::class) 79 | ->parent('open_telemetry.traces.processor_factory.abstract') 80 | 81 | ->set('open_telemetry.traces.processor_factory.simple', SimpleSpanProcessorFactory::class) 82 | ->parent('open_telemetry.traces.processor_factory.abstract') 83 | 84 | ->set('open_telemetry.traces.processor_interface', SpanProcessorInterface::class) 85 | 86 | // Providers 87 | ->set('open_telemetry.traces.provider_factory.abstract', AbstractTracerProviderFactory::class) 88 | ->abstract() 89 | ->args([ 90 | service('logger')->ignoreOnInvalid(), 91 | ]) 92 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 93 | 94 | ->set('open_telemetry.traces.provider_factory.noop', NoopTracerProviderFactory::class) 95 | ->parent('open_telemetry.traces.provider_factory.abstract') 96 | 97 | ->set('open_telemetry.traces.provider_factory.default', DefaultTracerProviderFactory::class) 98 | ->parent('open_telemetry.traces.provider_factory.abstract') 99 | 100 | ->set('open_telemetry.traces.provider_interface', TracerProviderInterface::class) 101 | 102 | // Tracer 103 | ->set('open_telemetry.traces.tracer_interface', TracerInterface::class) 104 | ; 105 | }; 106 | -------------------------------------------------------------------------------- /src/Resources/config/services_transports.php: -------------------------------------------------------------------------------- 1 | services() 16 | ->defaults() 17 | ->private() 18 | 19 | ->set('open_telemetry.transport_factory.abstract', AbstractTransportFactory::class) 20 | ->abstract() 21 | ->args([ 22 | service('logger')->ignoreOnInvalid(), 23 | ]) 24 | ->tag('monolog.logger', ['channel' => 'open_telemetry']) 25 | 26 | ->set('open_telemetry.transport_factory.grpc', GrpcTransportFactory::class) 27 | ->parent('open_telemetry.transport_factory.abstract') 28 | ->tag('open_telemetry.transport_factory') 29 | 30 | ->set('open_telemetry.transport_factory.otlp_http', OtlpHttpTransportFactory::class) 31 | ->parent('open_telemetry.transport_factory.abstract') 32 | ->tag('open_telemetry.transport_factory') 33 | 34 | ->set('open_telemetry.transport_factory.psr_http', PsrHttpTransportFactory::class) 35 | ->parent('open_telemetry.transport_factory.abstract') 36 | ->tag('open_telemetry.transport_factory') 37 | 38 | ->set('open_telemetry.transport_factory.stream', StreamTransportFactory::class) 39 | ->parent('open_telemetry.transport_factory.abstract') 40 | ->tag('open_telemetry.transport_factory') 41 | 42 | ->set('open_telemetry.transport_factory', TransportFactory::class) 43 | ->args([ 44 | tagged_iterator('open_telemetry.transport_factory'), 45 | ]) 46 | ; 47 | }; 48 | -------------------------------------------------------------------------------- /src/aliases.php: -------------------------------------------------------------------------------- 1 |