Skip to content

Drupal instrumentation - Mega pack #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4eb931b
Add EntityInstrumentation from https://github.com/previousnext/otel-c…
LionsAd Jan 22, 2025
dfdbb7e
Add AutoRootSpan to trace the whole request.
LionsAd Jan 22, 2025
c10235a
Add ViewsInstrumentation from previousnext/otel-contrib-drupal
LionsAd Jan 22, 2025
fe1d46d
Adjust ViewsInstrumentation to use InstrumentationBase.
LionsAd Jan 22, 2025
157c102
Add extra meta data for the view.
LionsAd Jan 22, 2025
eae9e99
Add trait to make instrumentation easier.
LionsAd Jan 23, 2025
7b5c0b0
Small fixes - fixup
LionsAd Jan 23, 2025
dea6e7f
Allow to use preHook and postHook closures for the helper as well.
LionsAd Jan 23, 2025
ffde2ba
Return early to avoid reflection when paramMap is empty.
LionsAd Jan 23, 2025
85c3433
Add CacheBackendInstrumentation
LionsAd Jan 24, 2025
eb0cdfe
Make CacheBackendInstrumentation use PerformanceX/InstrumentationTrait.
LionsAd Jan 27, 2025
773a6d6
Update CacheBackendInstrumentation.php to use InstrumentationBase2.php
LionsAd Jan 27, 2025
90bee58
Add new Instrumentation Base.
LionsAd Jan 27, 2025
3ac9b47
Add CacheBackendInstrumentation registration
LionsAd Jan 27, 2025
fc7d6aa
Add new base
LionsAd Jan 28, 2025
47ecff4
Refactor CacheBackend
LionsAd Jan 28, 2025
0b81f02
Add mandatory registerInstrumentation function.
LionsAd Jan 28, 2025
abf1b57
Adjust to new base class.
LionsAd Jan 28, 2025
83a30f8
Whitespace fixes
LionsAd Jan 28, 2025
86ece62
Add a way to instrument modules.
LionsAd Jan 28, 2025
d55cb46
AI refactor of Views.
LionsAd Jan 28, 2025
7dd2fb9
Remove setting spanKind to CLIENT (does not make sense)
LionsAd Jan 28, 2025
b274b96
Remove old InstrumentationTrait
LionsAd Jan 28, 2025
198a04b
Remove old InstrumentationBase
LionsAd Jan 28, 2025
6b2377f
Rename InstrumentationBase2 to InstrumentationBase
LionsAd Jan 28, 2025
72c5b95
Rename InstrumentationBase2 -> InstrumentationBase
LionsAd Jan 28, 2025
15ed0da
Make named parameters more flexible
LionsAd Jan 28, 2025
85ad58f
fixup: Rename InstrumentationBase2 -> InstrumentationBase
LionsAd Jan 28, 2025
63186bc
AI refactor to new base
LionsAd Jan 28, 2025
d562133
Use named parameters for EntityInstrumentation
LionsAd Jan 28, 2025
e067245
AI refactor of DatabaseInstrumentation
LionsAd Jan 28, 2025
60e64bb
Use named params for DatabaseInstrumentation
LionsAd Jan 28, 2025
bb3d306
AI Refactor of HttpClientRequestInstrumentation
LionsAd Jan 28, 2025
b7fa9d6
Fix url -> uri and set SPAN_CLIENT.
LionsAd Jan 28, 2025
a2493fd
AI Refactor of HttpClientCallInstrumentation
LionsAd Jan 28, 2025
02a6c57
AI Refactor of DrupalKernelInstrumentation
LionsAd Jan 28, 2025
e1ae70d
fix: PHPCBF findings
LionsAd Jan 28, 2025
13acb2e
Add PHPCS file
LionsAd Jan 28, 2025
28b57f8
Cleanup EntityInstrumentation
LionsAd Jan 28, 2025
58b3656
Fix FiberBoundContextStorage problem
LionsAd Jan 28, 2025
98cb6ac
fix: PHPCBF fixes
LionsAd Jan 28, 2025
6a5cef9
Make performance-x/opentelemetry-php-instrumentation-trait a requirement
LionsAd Jan 28, 2025
5406cb7
Pass through arguments.
LionsAd Jan 28, 2025
41fcc1a
Use new static::UPDATE_NAME functionality
LionsAd Jan 28, 2025
ca88e19
Ensure we have the new UPDATE_NAME constant.
LionsAd Jan 29, 2025
88f2e38
Automatically add EXPLAIN output
LionsAd Jan 28, 2025
20a2900
Ensure explain queries can be disabled.
LionsAd Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,11 @@ The extension can be disabled via runtime configuration:
OTEL_PHP_DISABLED_INSTRUMENTATIONS=drupal
```

### Query Explain Threshold Configuration

The EXPLAIN threshold feature allows you to set a minimum duration threshold for capturing EXPLAIN results in your traces. When `OTEL_PHP_DRUPAL_EXPLAIN_QUERIES` is enabled, you can use `OTEL_PHP_DRUPAL_EXPLAIN_THRESHOLD` to specify the minimum query duration (in milliseconds) that should trigger EXPLAIN capture.

For example:
```
OTEL_PHP_DRUPAL_EXPLAIN_THRESHOLD=100 # Capture EXPLAIN for queries taking longer than 100ms
```
24 changes: 21 additions & 3 deletions _register.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

declare(strict_types=1);

use OpenTelemetry\Contrib\Instrumentation\Drupal\CacheBackendInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\DatabaseInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\DrupalAutoRootSpan;
use OpenTelemetry\Contrib\Instrumentation\Drupal\DrupalKernelInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\EntityInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\InstrumentModules;
use OpenTelemetry\Contrib\Instrumentation\Drupal\HttpClientCallInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\HttpClientRequestInstrumentation;
use OpenTelemetry\Contrib\Instrumentation\Drupal\ViewsInstrumentation;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextStorage;
use OpenTelemetry\SDK\Sdk;

if (class_exists(Sdk::class) && Sdk::isInstrumentationDisabled(DrupalKernelInstrumentation::NAME) === TRUE) {
Expand All @@ -18,14 +25,25 @@
return;
}

// Force disable the FiberBoundContextStorage because of the conflict
// with Drupal Renderer service.
// @see https://www.drupal.org/project/opentelemetry/issues/3488173
// @todo Make a proper fix to work well with the FiberBoundContextStorage.
$contextStorage = new ContextStorage();
Context::setStorage($contextStorage);

try {
CacheBackendInstrumentation::register();
DrupalAutoRootSpan::register();
DrupalKernelInstrumentation::register();
DatabaseInstrumentation::register();
EntityInstrumentation::register();
HttpClientRequestInstrumentation::register();
HttpClientCallInstrumentation::register();
InstrumentModules::registerModule(ViewsInstrumentation::class);
}
catch (Throwable $exception) {
\Drupal::logger("drupalInstrumentation")->error($exception->getMessage());

return;
throw $exception;
//\Drupal::logger("drupalInstrumentation")->error($exception->getMessage());
//return;
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"open-telemetry/sdk": "^1",
"open-telemetry/sem-conv": "^1.23",
"symfony/http-client": "6.4.x-dev",
"nyholm/psr7": "^1.8@dev"
"nyholm/psr7": "^1.8@dev",
"performance-x/opentelemetry-php-instrumentation-trait": "^1.1.1"
},
"license": "Apache-2.0",
"minimum-stability": "dev",
Expand Down
57 changes: 57 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0"?>
<ruleset name="OpenTelemetry Instrumentation">
<description>Coding standards for OpenTelemetry Instrumentation Trait</description>

<!-- Use Drupal coding standards -->
<rule ref="Drupal">
<!-- Disable overly strict documentation rules -->
<exclude name="Drupal.Commenting.DocComment.MissingShort"/>
<exclude name="Drupal.Commenting.DocComment.Empty"/>
<exclude name="Drupal.Commenting.VariableComment.Missing"/>
<exclude name="Drupal.Commenting.FunctionComment.MissingParamComment"/>
<exclude name="Drupal.Commenting.FunctionComment.MissingReturnComment"/>
<exclude name="Drupal.Commenting.FunctionComment.ParamMissingDefinition"/>
<exclude name="Drupal.Classes.ClassFileName.NoMatch"/>
</rule>
<rule ref="DrupalPractice"/>

<!-- Scan these files -->
<file>./src/</file>
<file>./tests/</file>

<!-- PHP extensions to check -->
<arg name="extensions" value="php"/>

<!-- Drupal version -->
<config name="drupal_core_version" value="11"/>

<!-- Use 's' to print the full error message -->
<arg value="s"/>
<arg name="colors"/>

<!-- Two spaces for indentation -->
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="2"/>
<property name="tabIndent" value="false"/>
</properties>
</rule>

<!-- Line length -->
<rule ref="Drupal.Files.LineLength">
<properties>
<property name="lineLimit" value="100"/>
<property name="absoluteLineLimit" value="120"/>
</properties>
</rule>

<!-- Class/file naming -->
<rule ref="Drupal.NamingConventions.ValidClassName"/>

<!-- Ensure proper file doc blocks -->
<rule ref="Drupal.Commenting.FileComment"/>
<rule ref="Drupal.Commenting.DocComment"/>

<!-- PHP version compatibility -->
<config name="php_version" value="80000"/>
</ruleset>
116 changes: 116 additions & 0 deletions src/CacheBackendInstrumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace OpenTelemetry\Contrib\Instrumentation\Drupal;

use Drupal\Core\Cache\CacheBackendInterface;

/**
*
*/
class CacheBackendInstrumentation extends InstrumentationBase {
protected const CLASSNAME = CacheBackendInterface::class;
protected static array $cacheBins = [];

/**
*
*/
public static function register(): void {
static::create(
name: 'io.opentelemetry.contrib.php.drupal',
prefix: 'drupal.cache',
className: static::CLASSNAME
);
}

/**
*
*/
protected function registerInstrumentation(): void {
// Capture bin name in constructor.
$this->helperHook(
methodName: '__construct',
preHandler: function ($spanBuilder, $object, array $params, string $class) {
// Get actual constructor parameters at runtime.
$reflection = new \ReflectionClass($class);
$constructor = $reflection->getConstructor();
if (!$constructor) {
return;
}

foreach ($constructor->getParameters() as $position => $parameter) {
if ($parameter->getName() === 'bin' && isset($params[$position])) {
$bin = $params[$position];
$spanBuilder->setAttribute($this->getAttributeName('bin'), $bin);
static::$cacheBins[spl_object_id($object)] = $bin;
break;
}
}
}
);

// Define operations.
$operations = [
'get' => [
'params' => ['cid' => 'cache_key'],
'postHandler' => function ($span, $object, array $namedParams, $returnValue) {
$span->setAttribute($this->getAttributeName('hit'), $returnValue !== FALSE);
$span->setAttribute($this->getAttributeName('valid'), !empty($returnValue->valid));
},
],
'getMultiple' => [
'params' => ['cids' => 'cache_keys'],
'postHandler' => function ($span, $object, array $namedParams, $returnValue) {
$missCount = count($namedParams['cids']);
$hitCount = count($returnValue);
$total = $missCount + $hitCount;

$span->setAttribute($this->getAttributeName('hit_count'), $hitCount);
$span->setAttribute($this->getAttributeName('miss_count'), $missCount);
$span->setAttribute($this->getAttributeName('hit_ratio'), $total > 0 ? $hitCount / $total : 0);
},
],
'set' => [
'params' => ['cid' => 'cache_key', 'tags'],
'preHandler' => function ($spanBuilder, $object, array $namedParams) {
$spanBuilder->setAttribute($this->getAttributeName('ttl'), isset($namedParams['expire']) ? $namedParams['expire'] : -1);
$spanBuilder->setAttribute($this->getAttributeName('tag_count'), count($namedParams['tags'] ?? []));
},
],
'deleteMultiple' => [
'params' => ['cids' => 'cache_keys'],
'preHandler' => function ($spanBuilder, $object, array $namedParams) {
$spanBuilder->setAttribute($this->getAttributeName('delete_count'), count($namedParams['cids']));
},
],
'invalidateMultiple' => [
'params' => ['cids' => 'cache_keys'],
'preHandler' => function ($spanBuilder, $object, array $namedParams) {
$spanBuilder->setAttribute($this->getAttributeName('invalidate_count'), count($namedParams['cids']));
},
],
'delete' => [
'params' => ['cid' => 'cache_key'],
],
'invalidate' => [
'params' => ['cid' => 'cache_key'],
],
'deleteAll' => [],
'invalidateAll' => [],
'removeBin' => [],
];

// Common handler for adding bin information.
$binHandler = function ($spanBuilder, $object) {
$objectId = spl_object_id($object);
$bin = static::$cacheBins[$objectId] ?? 'unknown';
$spanBuilder->setAttribute($this->getAttributeName('bin'), $bin);
};

// Register all operations with common bin handling.
$this->registerOperations(
operations: $operations,
commonPreHandler: $binHandler
);
}

}
126 changes: 91 additions & 35 deletions src/DatabaseInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,113 @@
namespace OpenTelemetry\Contrib\Instrumentation\Drupal;

use Drupal\Core\Database\Connection;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SemConv\TraceAttributes;
use function \OpenTelemetry\Instrumentation\hook;

/**
* Provides OpenTelemetry instrumentation for Drupal database operations.
*/
class DatabaseInstrumentation extends InstrumentationBase {

protected const CLASSNAME = Connection::class;
public const DB_VARIABLES = 'db.variables';

/**
*
*/
public static function register(): void {
static::create(
name: 'io.opentelemetry.contrib.php.drupal',
prefix: 'drupal.database',
className: static::CLASSNAME
);
}

$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.drupal');
protected bool $inQuery = FALSE;

hook(
Connection::class,
'query',
static::preClosure($instrumentation),
static::postClosure()
);
protected bool $explainQueries = FALSE;
/**
* Minimum duration threshold (in seconds) for capturing EXPLAIN results.
*
* When explainQueries is enabled, only queries that take longer than this
* threshold will have their EXPLAIN results captured in the span. This helps
* prevent unnecessary overhead for fast queries.
*
* Configure via OTEL_PHP_DRUPAL_EXPLAIN_THRESHOLD environment variable.
* Value should be in milliseconds, e.g.:
* OTEL_PHP_DRUPAL_EXPLAIN_THRESHOLD=100 // For 100ms threshold
*
* @var float Time in seconds (e.g., 0.1 for 100ms)
*/
protected float $explainQueriesThreshold = 0.0;

/**
*
*/
protected function registerInstrumentation(): void {

$this->explainQueries = getenv('OTEL_PHP_DRUPAL_EXPLAIN_QUERIES') ? TRUE : FALSE;
$this->explainQueriesThreshold = (float) (getenv('OTEL_PHP_DRUPAL_EXPLAIN_THRESHOLD') ?? 0) / 1000;

$operations = [
'query' => [
'preHandler' => function ($spanBuilder, Connection $connection, array $namedParams) {
if ($this->inQuery) {
return;
}

$spanBuilder
->setSpanKind(SpanKind::KIND_CLIENT)
->setAttribute(TraceAttributes::DB_SYSTEM, 'mariadb')
->setAttribute(TraceAttributes::DB_STATEMENT, $namedParams['query']);

if (isset($namedParams['args']) === TRUE) {
$cleanVariables = array_map(
static fn ($value) => is_array($value) ? json_encode($value) : (string) $value,
$namedParams['args']
);
$spanBuilder->setAttribute(self::DB_VARIABLES, $cleanVariables);
}

if ($this->explainQueries) {
$this->captureQueryExplain(
$spanBuilder,
$connection,
$namedParams['query'],
$namedParams['args'] ?? []
);
}
},
],
];

$this->registerOperations($operations);
}

/**
* @param \OpenTelemetry\API\Instrumentation\CachedInstrumentation $instrumentation
* Captures and adds EXPLAIN query results as attributes to the span builder.
*
* @return \Closure
* This function executes an EXPLAIN query and adds the results as attributes
* to the span builder if the query execution time exceeds the configured threshold.
*
* @param mixed $spanBuilder The span builder instance
* @param \Drupal\Core\Database\Connection $connection Database connection
* @param string $query The SQL query to explain
* @param array $args Query arguments
*/
public static function preClosure(CachedInstrumentation $instrumentation): \Closure {
return static function (Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
$parent = Context::getCurrent();

/** @var \OpenTelemetry\API\Trace\SpanBuilderInterface $span */
$span = $instrumentation->tracer()->spanBuilder('database::query')
->setParent($parent)
->setSpanKind(SpanKind::KIND_CLIENT)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno)
->setAttribute(TraceAttributes::DB_SYSTEM, 'mariadb')
->setAttribute(TraceAttributes::DB_STATEMENT, $params[0]);
if (isset($params[1]) === TRUE) {
$cleanVariables = array_map(static fn ($value) => is_array($value) ? json_encode($value) : (string) $value, $params[1]);
$span->setAttribute(self::DB_VARIABLES, $cleanVariables);
}
$span = $span->startSpan();
protected function captureQueryExplain($spanBuilder, Connection $connection, string $query, array $args): void {
$this->inQuery = TRUE;
try {
$start = microtime(true);
$explain_results = $connection->query('EXPLAIN ' . $query, $args)->fetchAll();
$duration = microtime(true) - $start;

Context::storage()
->attach($span->storeInContext($parent));
};
if ($duration >= $this->explainQueriesThreshold) {
$spanBuilder->setAttribute($this->getAttributeName('explain'), json_encode($explain_results));
}
}
catch (\Exception $e) {
}
$this->inQuery = FALSE;
}

}
Loading