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 @@
+[](https://github.com/opentelemetry-php/contrib-auto-opcache/releases)
+[](https://github.com/open-telemetry/opentelemetry-php/issues)
+[](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/opcache)
+[](https://github.com/opentelemetry-php/contrib-auto-opcache)
+[](https://packagist.org/packages/open-telemetry/opentelemetry-auto-opcache/)
+[](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