diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 273275062..9d786a7dd 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -41,6 +41,7 @@ jobs: 'Instrumentation/MySqli', 'Instrumentation/OpenAIPHP', 'Instrumentation/PDO', + 'Instrumentation/Opcache', 'Instrumentation/PostgreSql', # Sort PSRs numerically. 'Instrumentation/Psr3', @@ -84,6 +85,7 @@ jobs: php-version: 8.1 - project: 'Instrumentation/PostgreSql' php-version: 8.1 + - project: 'Instrumentation/Opcache' - project: 'Instrumentation/Session' php-version: 8.1 steps: diff --git a/.gitsplit.yml b/.gitsplit.yml index eac359c84..3ae31fc2d 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -44,6 +44,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-openai.git" - prefix: "src/Instrumentation/PDO" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-pdo.git" + - prefix: "src/Instrumentation/opcache" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-opcache.git" - prefix: "src/Instrumentation/PostgreSql" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-postgresql.git" - prefix: "src/Instrumentation/Psr3" diff --git a/composer.json b/composer.json index 5591c3d21..c2d4bedb1 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "OpenTelemetry\\Contrib\\Instrumentation\\MySqli\\": "src/Instrumentation/MySqli/src", "OpenTelemetry\\Contrib\\Instrumentation\\OpenAIPHP\\": "src/Instrumentation/OpenAIPHP/src", "OpenTelemetry\\Contrib\\Instrumentation\\PDO\\": "src/Instrumentation/PDO/src", + "OpenTelemetry\\Contrib\\Instrumentation\\Opcache\\": "src/Instrumentation/Opcache/src", "OpenTelemetry\\Contrib\\Instrumentation\\Psr3\\": "src/Instrumentation/Psr3/src", "OpenTelemetry\\Contrib\\Instrumentation\\Psr6\\": "src/Instrumentation/Psr6/src", "OpenTelemetry\\Contrib\\Instrumentation\\Psr14\\": "src/Instrumentation/Psr14/src", @@ -75,6 +76,7 @@ "src/Instrumentation/MySqli/_register.php", "src/Instrumentation/OpenAIPHP/_register.php", "src/Instrumentation/PDO/_register.php", + "src/Instrumentation/Opcache/_register.php", "src/Instrumentation/Psr3/_register.php", "src/Instrumentation/Psr6/_register.php", "src/Instrumentation/Psr14/_register.php", @@ -113,6 +115,7 @@ "OpenTelemetry\\Tests\\Instrumentation\\MySqli\\": "src/Instrumentation/MySqli/tests", "OpenTelemetry\\Contrib\\Instrumentation\\OpenAIPHP\\Tests\\": "src/Instrumentation/OpenAIPHP/tests", "OpenTelemetry\\Tests\\Instrumentation\\PDO\\": "src/Instrumentation/PDO/tests", + "OpenTelemetry\\Tests\\Instrumentation\\Opcache\\": "src/Instrumentation/Opcache/tests", "OpenTelemetry\\Tests\\Instrumentation\\Psr6\\": "src/Instrumentation/Psr6/tests", "OpenTelemetry\\Instrumentation\\Psr14\\Tests\\": "src/Instrumentation/Psr14/tests", "OpenTelemetry\\Tests\\Instrumentation\\Psr16\\": "src/Instrumentation/Psr16/tests", @@ -152,6 +155,7 @@ "open-telemetry/opentelemetry-auto-mysqli": "self.version", "open-telemetry/opentelemetry-auto-openai-php": "self.version", "open-telemetry/opentelemetry-auto-pdo": "self.version", + "open-telemetry/opentelemetry-auto-opcache": "self.version", "open-telemetry/opentelemetry-auto-psr3": "self.version", "open-telemetry/opentelemetry-auto-psr6": "self.version", "open-telemetry/opentelemetry-auto-psr14": "self.version", diff --git a/docker-compose.yaml b/docker-compose.yaml index a41f04f55..cfd4ab783 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,6 +4,7 @@ services: image: ghcr.io/open-telemetry/opentelemetry-php/opentelemetry-php-base:${PHP_VERSION} volumes: - ./:/usr/src/myapp + - ./docker/opcache/opcache.ini:/etc/php/${PHP_VERSION}/cli/conf.d/opcache.ini user: "${PHP_USER}:root" environment: XDEBUG_MODE: ${XDEBUG_MODE:-off} diff --git a/docker/opcache/opcache.ini b/docker/opcache/opcache.ini new file mode 100644 index 000000000..f060f8e72 --- /dev/null +++ b/docker/opcache/opcache.ini @@ -0,0 +1,11 @@ +; OPcache configuration +[opcache] +opcache.enable=1 +opcache.enable_cli=1 +opcache.memory_consumption=128 +opcache.interned_strings_buffer=8 +opcache.max_accelerated_files=10000 +opcache.revalidate_freq=60 +opcache.validate_timestamps=1 +opcache.save_comments=1 +opcache.enable_file_override=0 diff --git a/src/Instrumentation/Opcache/.gitattributes b/src/Instrumentation/Opcache/.gitattributes new file mode 100644 index 000000000..1676cf825 --- /dev/null +++ b/src/Instrumentation/Opcache/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/src/Instrumentation/Opcache/.gitignore b/src/Instrumentation/Opcache/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/src/Instrumentation/Opcache/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/Instrumentation/Opcache/.php-cs-fixer.php b/src/Instrumentation/Opcache/.php-cs-fixer.php new file mode 100644 index 000000000..e35fa078c --- /dev/null +++ b/src/Instrumentation/Opcache/.php-cs-fixer.php @@ -0,0 +1,43 @@ +exclude('vendor') + ->exclude('var/cache') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'is_null' => true, + 'modernize_types_casting' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'single_line_comment_style' => true, + 'yoda_style' => false, + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'declare_strict_types' => true, + 'type_declaration_spaces' => true, + 'include' => true, + 'lowercase_cast' => true, + 'new_with_parentheses' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'echo_tag_syntax' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_types' => true, + 'short_scalar_cast' => true, + 'blank_lines_before_namespace' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); + diff --git a/src/Instrumentation/Opcache/README.md b/src/Instrumentation/Opcache/README.md new file mode 100644 index 000000000..e4f5faec0 --- /dev/null +++ b/src/Instrumentation/Opcache/README.md @@ -0,0 +1,78 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-opcache/releases) +[![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues) +[![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/opcache) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-opcache) +[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-opcache/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-opcache/) +[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-opcache/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-opcache/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry PHP OPcache Instrumentation + +Please read https://opentelemetry.io/docs/instrumentation/php/automatic/ for instructions on how to +install and configure the extension and SDK. + +## Overview + +This instrumentation package captures PHP OPcache metrics and adds them as attributes to the active span. +It automatically registers a shutdown function to collect OPcache metrics at the end of the request. + +The following OPcache metrics are captured: + +### Basic Status +- `opcache.enabled` - Whether OPcache is enabled +- `opcache.available` - Whether OPcache is available + +### Memory Usage +- `opcache.memory.used_bytes` - Memory used by OPcache in bytes +- `opcache.memory.free_bytes` - Free memory available to OPcache in bytes +- `opcache.memory.wasted_bytes` - Wasted memory in bytes +- `opcache.memory.used_percentage` - Percentage of total memory used +- `opcache.memory.wasted_percentage` - Percentage of total memory wasted + +### Cache Statistics +- `opcache.scripts.cached` - Number of cached scripts +- `opcache.hits.total` - Total number of cache hits +- `opcache.misses.total` - Total number of cache misses +- `opcache.hit_rate.percentage` - Cache hit rate percentage +- `opcache.keys.cached` - Number of cached keys +- `opcache.keys.max_cached` - Maximum number of cached keys + +### Restart Statistics +- `opcache.restarts.oom` - Number of out-of-memory restarts +- `opcache.restarts.hash` - Number of hash restarts +- `opcache.restarts.manual` - Number of manual restarts + +### Interned Strings +- `opcache.interned_strings.buffer_size` - Interned strings buffer size +- `opcache.interned_strings.used_memory` - Memory used by interned strings +- `opcache.interned_strings.free_memory` - Free memory for interned strings +- `opcache.interned_strings.strings_count` - Number of interned strings +- `opcache.interned_strings.usage_percentage` - Percentage of interned strings buffer used + +## Usage + +The instrumentation is automatically registered via composer. No additional configuration is required. + +You can also manually add OPcache metrics to the current active span: + +```php +use OpenTelemetry\Contrib\Instrumentation\opcache\opcacheInstrumentation; + +// Add OPcache metrics to the current active span +opcacheInstrumentation::addOpcacheMetricsToRootSpan(); +``` + +## Configuration + +The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration): + +```shell +OTEL_PHP_DISABLED_INSTRUMENTATIONS=opcache +``` + +## Requirements + +- PHP 8.0 or higher +- OPcache extension +- OpenTelemetry extension diff --git a/src/Instrumentation/Opcache/_register.php b/src/Instrumentation/Opcache/_register.php new file mode 100644 index 000000000..3475ed5c5 --- /dev/null +++ b/src/Instrumentation/Opcache/_register.php @@ -0,0 +1,18 @@ + + + + + + + src + + + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + diff --git a/src/Instrumentation/Opcache/psalm.xml.dist b/src/Instrumentation/Opcache/psalm.xml.dist new file mode 100644 index 000000000..155711712 --- /dev/null +++ b/src/Instrumentation/Opcache/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/Instrumentation/Opcache/src/OpcacheInstrumentation.php b/src/Instrumentation/Opcache/src/OpcacheInstrumentation.php new file mode 100644 index 000000000..25a1e48d2 --- /dev/null +++ b/src/Instrumentation/Opcache/src/OpcacheInstrumentation.php @@ -0,0 +1,139 @@ +getContext()->isValid()) { + return; // No active span, nothing to do + } + + self::captureOpcacheMetrics($span); + } + + private static function captureOpcacheMetrics(SpanInterface $span): void + { + if (!function_exists('opcache_get_status')) { + $span->setAttribute('opcache.enabled', false); + + return; + } + + $status = @opcache_get_status(false); + if (!$status) { + $span->setAttribute('opcache.available', false); + + return; + } + + $span->setAttribute('opcache.enabled', true); + $span->setAttribute('opcache.available', true); + + // Memory metrics + if (isset($status['memory_usage'])) { + $memory = $status['memory_usage']; + self::addMemoryAttributes($span, $memory); + } + + // Statistics metrics + if (isset($status['opcache_statistics'])) { + $stats = $status['opcache_statistics']; + self::addStatisticsAttributes($span, $stats); + } + + // Interned strings metrics + if (isset($status['interned_strings_usage'])) { + $interned = $status['interned_strings_usage']; + self::addInternedStringsAttributes($span, $interned); + } + } + + private static function addMemoryAttributes(SpanInterface $span, array $memory): void + { + $span->setAttribute('opcache.memory.used_bytes', $memory['used_memory'] ?? 0); + $span->setAttribute('opcache.memory.free_bytes', $memory['free_memory'] ?? 0); + $span->setAttribute('opcache.memory.wasted_bytes', $memory['wasted_memory'] ?? 0); + + $used = $memory['used_memory'] ?? 0; + $free = $memory['free_memory'] ?? 0; + $wasted = $memory['wasted_memory'] ?? 0; + $total = (int) $used + (int) $free + (int) $wasted; + + if ($total > 0) { + $span->setAttribute('opcache.memory.used_percentage', round(($used / $total) * 100, 2)); + $span->setAttribute('opcache.memory.wasted_percentage', round(($wasted / $total) * 100, 2)); + } + } + + private static function addStatisticsAttributes(SpanInterface $span, array $stats): void + { + $span->setAttribute('opcache.scripts.cached', $stats['num_cached_scripts'] ?? 0); + $span->setAttribute('opcache.hits.total', $stats['hits'] ?? 0); + $span->setAttribute('opcache.misses.total', $stats['misses'] ?? 0); + + $hits = (int) ($stats['hits'] ?? 0); + $misses = (int) ($stats['misses'] ?? 0); + $total = $hits + $misses; + + if ($total > 0) { + $span->setAttribute('opcache.hit_rate.percentage', round(($hits / $total) * 100, 2)); + } + + // Restart metrics + $span->setAttribute('opcache.restarts.oom', $stats['oom_restarts'] ?? 0); + $span->setAttribute('opcache.restarts.hash', $stats['hash_restarts'] ?? 0); + $span->setAttribute('opcache.restarts.manual', $stats['manual_restarts'] ?? 0); + + // Cache key metrics + $span->setAttribute('opcache.keys.cached', $stats['num_cached_keys'] ?? 0); + $span->setAttribute('opcache.keys.max_cached', $stats['max_cached_keys'] ?? 0); + } + + private static function addInternedStringsAttributes(SpanInterface $span, array $interned): void + { + $span->setAttribute('opcache.interned_strings.buffer_size', $interned['buffer_size'] ?? 0); + $span->setAttribute('opcache.interned_strings.used_memory', $interned['used_memory'] ?? 0); + $span->setAttribute('opcache.interned_strings.free_memory', $interned['free_memory'] ?? 0); + $span->setAttribute('opcache.interned_strings.strings_count', $interned['number_of_strings'] ?? 0); + + $used = $interned['used_memory'] ?? 0; + $size = $interned['buffer_size'] ?? 0; + + if ($size > 0) { + $span->setAttribute('opcache.interned_strings.usage_percentage', round(($used / $size) * 100, 2)); + } + } +} diff --git a/src/Instrumentation/Opcache/tests/Integration/OpcacheInstrumentationTest.php b/src/Instrumentation/Opcache/tests/Integration/OpcacheInstrumentationTest.php new file mode 100644 index 000000000..d12bc41c5 --- /dev/null +++ b/src/Instrumentation/Opcache/tests/Integration/OpcacheInstrumentationTest.php @@ -0,0 +1,122 @@ +storage = new ArrayObject(); + $this->tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + new InMemoryExporter($this->storage) + ) + ); + + $this->scope = Configurator::create() + ->withTracerProvider($this->tracerProvider) + ->activate(); + } + + public function tearDown(): void + { + $this->scope->detach(); + } + + public function test_add_opcache_metrics_to_root_span(): void + { + + // Create a root span + $tracer = $this->tracerProvider->getTracer('test'); + $rootSpan = $tracer->spanBuilder('root_span') + ->setSpanKind(SpanKind::KIND_SERVER) + ->startSpan(); + + // Activate the root span + $scope = $rootSpan->activate(); + + try { + // Call the method to add opcache metrics to the root span + opcacheInstrumentation::addOpcacheMetricsToRootSpan(); + + // End the span + $rootSpan->end(); + + // Verify that the span has opcache attributes + $this->assertCount(1, $this->storage); + $span = $this->storage->offsetGet(0); + + // At minimum, it should have the opcache.enabled attribute + $attributes = $span->getAttributes(); + $this->assertTrue($attributes->has('opcache.enabled')); + + // If OPcache is enabled and available, check for more attributes + if (function_exists('opcache_get_status') && @opcache_get_status(false)) { + $this->assertTrue($attributes->has('opcache.available')); + $this->assertTrue($attributes->has('opcache.memory.used_bytes')); + $this->assertTrue($attributes->has('opcache.memory.free_bytes')); + $this->assertTrue($attributes->has('opcache.memory.wasted_bytes')); + $this->assertTrue($attributes->has('opcache.scripts.cached')); + $this->assertTrue($attributes->has('opcache.hits.total')); + $this->assertTrue($attributes->has('opcache.misses.total')); + } + } finally { + $scope->detach(); + } + } + + public function test_capture_opcache_metrics(): void + { + + // Create a span + $tracer = $this->tracerProvider->getTracer('test'); + $span = $tracer->spanBuilder('test_span')->startSpan(); + + // Call the captureOpcacheMetrics method using reflection + $reflectionClass = new ReflectionClass(opcacheInstrumentation::class); + $method = $reflectionClass->getMethod('captureOpcacheMetrics'); + //$method->setAccessible(true); + $method->invoke(null, $span); + + // End the span + $span->end(); + + // Verify that the span has opcache attributes + $this->assertCount(1, $this->storage); + $storedSpan = $this->storage->offsetGet(0); + $attributes = $storedSpan->getAttributes(); + + // At minimum, it should have the opcache.enabled attribute + $this->assertTrue($attributes->has('opcache.enabled')); + + // If OPcache is enabled and available, check for more attributes + if (function_exists('opcache_get_status') && @opcache_get_status(false)) { + $this->assertTrue($attributes->has('opcache.available')); + $this->assertTrue($attributes->has('opcache.memory.used_bytes')); + $this->assertTrue($attributes->has('opcache.memory.free_bytes')); + $this->assertTrue($attributes->has('opcache.memory.wasted_bytes')); + $this->assertTrue($attributes->has('opcache.scripts.cached')); + $this->assertTrue($attributes->has('opcache.hits.total')); + $this->assertTrue($attributes->has('opcache.misses.total')); + } + } + +} diff --git a/src/Instrumentation/Opcache/tests/Unit/.gitkeep b/src/Instrumentation/Opcache/tests/Unit/.gitkeep new file mode 100644 index 000000000..e69de29bb