Skip to content

Commit

Permalink
feat: Add CakePHP 3+ Support (#2618)
Browse files Browse the repository at this point in the history
* feat: Add CakePHP 3 Support

* tests: Test for 7.1+ (requires intl) + typo

* feat: Retrieve extension, plugin, and theme information

* tests: Add CakePHP v4.5 tests

* tests: Add CakePHP v5.0 tests

* style: Use `hook_method`

* fix: Fetch root span from `\DDTrace\root_span()`

* style: refactor common logic
  • Loading branch information
PROFeNoM committed Apr 15, 2024
1 parent e721e34 commit bbd3a9c
Show file tree
Hide file tree
Showing 285 changed files with 12,634 additions and 103 deletions.
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ TEST_INTEGRATIONS_71 := \
TEST_WEB_71 := \
test_metrics \
test_web_cakephp_28 \
test_web_cakephp_310 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_laravel_42 \
Expand Down Expand Up @@ -610,6 +611,7 @@ TEST_INTEGRATIONS_72 := \

TEST_WEB_72 := \
test_metrics \
test_web_cakephp_310 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_89 \
Expand Down Expand Up @@ -669,6 +671,7 @@ TEST_INTEGRATIONS_73 :=\

TEST_WEB_73 := \
test_metrics \
test_web_cakephp_310 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_89 \
Expand Down Expand Up @@ -730,6 +733,8 @@ TEST_INTEGRATIONS_74 := \

TEST_WEB_74 := \
test_metrics \
test_web_cakephp_310 \
test_web_cakephp_45 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_89 \
Expand Down Expand Up @@ -792,6 +797,7 @@ TEST_INTEGRATIONS_80 := \

TEST_WEB_80 := \
test_metrics \
test_web_cakephp_45 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_95 \
Expand Down Expand Up @@ -840,6 +846,8 @@ TEST_INTEGRATIONS_81 := \

TEST_WEB_81 := \
test_metrics \
test_web_cakephp_45 \
test_web_cakephp_50 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_95 \
Expand Down Expand Up @@ -891,6 +899,8 @@ TEST_INTEGRATIONS_82 := \

TEST_WEB_82 := \
test_metrics \
test_web_cakephp_45 \
test_web_cakephp_50 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_95 \
Expand Down Expand Up @@ -946,6 +956,8 @@ TEST_INTEGRATIONS_83 := \

TEST_WEB_83 := \
test_metrics \
test_web_cakephp_45 \
test_web_cakephp_50 \
test_web_codeigniter_22 \
test_web_codeigniter_31 \
test_web_drupal_95 \
Expand Down Expand Up @@ -1230,6 +1242,15 @@ test_integrations_swoole_5: global_test_run_dependencies
test_web_cakephp_28: global_test_run_dependencies
$(call run_composer_with_retry,tests/Frameworks/CakePHP/Version_2_8,)
$(call run_tests_debug,--testsuite=cakephp-28-test)
test_web_cakephp_310: global_test_run_dependencies
$(call run_composer_with_retry,tests/Frameworks/CakePHP/Version_3_10,)
$(call run_tests_debug,--testsuite=cakephp-310-test)
test_web_cakephp_45: global_test_run_dependencies
$(call run_composer_with_retry,tests/Frameworks/CakePHP/Version_4_5,)
$(call run_tests_debug,--testsuite=cakephp-45-test)
test_web_cakephp_50: global_test_run_dependencies
$(call run_composer_with_retry,tests/Frameworks/CakePHP/Version_5_0,)
$(call run_tests_debug,--testsuite=cakephp-50-test)
test_web_codeigniter_22: global_test_run_dependencies
$(call run_tests_debug,--testsuite=codeigniter-22-test)
test_web_codeigniter_31: global_test_run_dependencies
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"sort-packages": true,
"allow-plugins": {
"g1a/composer-test-scenarios": true,
"php-http/discovery": true
"php-http/discovery": true,
"cakephp/plugin-installer": true
}
},
"autoload": {
Expand Down
4 changes: 4 additions & 0 deletions ext/integrations/integrations.c
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ void ddtrace_integrations_minit(void) {
"DDTrace\\Integrations\\CakePHP\\CakePHPIntegration");
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_CAKEPHP, "Dispatcher", "__construct",
"DDTrace\\Integrations\\CakePHP\\CakePHPIntegration");
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_CAKEPHP, "App\\Application", "__construct",
"DDTrace\\Integrations\\CakePHP\\CakePHPIntegration");
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_CAKEPHP, "Cake\\Http\\Server", "__construct",
"DDTrace\\Integrations\\CakePHP\\CakePHPIntegration");

DD_SET_UP_DEFERRED_LOADING_BY_FUNCTION(DDTRACE_INTEGRATION_EXEC, "exec",
"DDTrace\\Integrations\\Exec\\ExecIntegration");
Expand Down
140 changes: 39 additions & 101 deletions src/DDTrace/Integrations/CakePHP/CakePHPIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

namespace DDTrace\Integrations\CakePHP;

use CakeRequest;
use DDTrace\Integrations\CakePHP\V2\CakePHPIntegrationLoader as CakePHPIntegrationLoaderV2;
use DDTrace\Integrations\CakePHP\V3\CakePHPIntegrationLoader as CakePHPIntegrationLoaderV3;
use DDTrace\Integrations\Integration;
use DDTrace\SpanData;
use DDTrace\Tag;
use DDTrace\Type;
use DDTrace\Util\Normalizer;
use Router;

class CakePHPIntegration extends Integration
{
const NAME = 'cakephp';

public $appName;
public $rootSpan;
public $setRootSpanInfoFn;
public $handleExceptionFn;
public $setStatusCodeFn;
public $parseRouteFn;

/**
* @return string The integration name.
Expand All @@ -25,122 +26,59 @@ public function getName()
return self::NAME;
}

// CakePHP v2.x - we don't need to check for v3 since it does not have \Dispatcher or \ShellDispatcher
public function init(): int
{
$integration = $this;

// Since "Dispatcher" and "App" are common names, check for a CakePHP signature before loading
if (!defined('CAKE_CORE_INCLUDE_PATH')) {
return self::NOT_AVAILABLE;
}

$integration->rootSpan = null;

$setRootSpanInfoFn = function () use ($integration) {
$integration->setRootSpanInfoFn = function () use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan === null) {
return;
}

$integration->appName = \ddtrace_config_app_name(CakePHPIntegration::NAME);
$integration->rootSpan = $rootSpan;
$integration->addTraceAnalyticsIfEnabled($integration->rootSpan);
$integration->rootSpan->service = $integration->appName;
$integration->addTraceAnalyticsIfEnabled($rootSpan);
$rootSpan->service = $integration->appName;
if ('cli' === PHP_SAPI) {
$integration->rootSpan->name = 'cakephp.console';
$integration->rootSpan->resource =
!empty($_SERVER['argv'][1]) ? 'cake_console ' . $_SERVER['argv'][1] : 'cake_console';
$rootSpan->name = 'cakephp.console';
$rootSpan->resource = !empty($_SERVER['argv'][1])
? 'cake_console ' . $_SERVER['argv'][1]
: 'cake_console';
} else {
$integration->rootSpan->name = 'cakephp.request';
$integration->rootSpan->meta[Tag::SPAN_KIND] = 'server';
$rootSpan->name = 'cakephp.request';
$rootSpan->meta[Tag::SPAN_KIND] = 'server';
}
$integration->rootSpan->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
$rootSpan->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
};

\DDTrace\hook_method('App', 'init', $setRootSpanInfoFn);
\DDTrace\hook_method('Dispatcher', '__construct', $setRootSpanInfoFn);

\DDTrace\trace_method(
'Controller',
'invokeAction',
function (SpanData $span, array $args) use ($integration) {
$span->name = $span->resource = 'Controller.invokeAction';
$span->type = Type::WEB_SERVLET;
$span->service = $integration->appName;
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;

$request = $args[0];
if (!$request instanceof CakeRequest) {
return;
}

if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) {
$integration->rootSpan->resource =
$_SERVER['REQUEST_METHOD'] . ' ' . $this->name . 'Controller@' . $request->params['action'];
}

if (!array_key_exists(Tag::HTTP_URL, $integration->rootSpan->meta)) {
$integration->rootSpan->meta[Tag::HTTP_URL] = Router::url($request->here, true)
. Normalizer::sanitizedQueryString();
}
$integration->rootSpan->meta['cakephp.route.controller'] = $request->params['controller'];
$integration->rootSpan->meta['cakephp.route.action'] = $request->params['action'];
if (isset($request->params['plugin'])) {
$integration->rootSpan->meta['cakephp.plugin'] = $request->params['plugin'];
}
$integration->handleExceptionFn = function ($This, $scope, $args) use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan !== null) {
$integration->setError($rootSpan, $args[0]);
}
);

// This only traces the default exception renderer
// Remove this when error tracking is added
// Other possible places to trace
// - ErrorHandler::handleException()
// - Controller::appError()
// - Exception.handler
// - Exception.renderer
\DDTrace\trace_method('ExceptionRenderer', '__construct', [
'instrument_when_limited' => 1,
'posthook' => function (SpanData $span, array $args) use ($integration) {
$integration->setError($integration->rootSpan, $args[0]);
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
return false;
},
]);

\DDTrace\trace_method('CakeResponse', 'statusCode', [
'instrument_when_limited' => 1,
'posthook' => function (SpanData $span, $args, $return) use ($integration) {
$integration->rootSpan->meta[Tag::HTTP_STATUS_CODE] = $return;
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
return false;
},
]);
};

// Create a trace span for every template rendered
\DDTrace\trace_method('View', 'render', function (SpanData $span) use ($integration) {
$span->name = 'cakephp.view';
$span->type = Type::WEB_SERVLET;
$file = $this->viewPath . '/' . $this->view . $this->ext;
$span->resource = $file;
$span->meta = ['cakephp.view' => $file];
$span->service = $integration->appName;
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
});
$integration->setStatusCodeFn = function ($This, $scope, $args, $retval) use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan) {
$rootSpan->meta[Tag::HTTP_STATUS_CODE] = $retval;
}
};

\DDTrace\hook_method(
'CakeRoute',
'parse',
null,
function ($app, $appClass, $args, $retval) use ($integration) {
if (!$retval) {
return;
}
$integration->parseRouteFn = function ($app, $appClass, $args, $retval) use ($integration) {
if (!$retval) {
return;
}

$integration->rootSpan->meta[Tag::HTTP_ROUTE] = $app->template;
$rootSpan = \DDTrace\root_span();
if ($rootSpan !== null) {
$rootSpan->meta[Tag::HTTP_ROUTE] = $app->template;
}
);
};

return Integration::LOADED;
$loader = class_exists('Cake\Http\Server') // Only exists in V3+
? new CakePHPIntegrationLoaderV3()
: new CakePHPIntegrationLoaderV2();
return $loader->load($integration);
}
}
99 changes: 99 additions & 0 deletions src/DDTrace/Integrations/CakePHP/V2/CakePHPIntegrationLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace DDTrace\Integrations\CakePHP\V2;

use CakeRequest;
use DDTrace\Integrations\CakePHP\CakePHPIntegration;
use DDTrace\Integrations\Integration;
use DDTrace\SpanData;
use DDTrace\Tag;
use DDTrace\Type;
use DDTrace\Util\Normalizer;
use Router;

class CakePHPIntegrationLoader
{
// CakePHP v2.x - we don't need to check for v3 since it does not have \Dispatcher or \ShellDispatcher
public function load($integration)
{
if (!defined('CAKE_CORE_INCLUDE_PATH')) {
return Integration::NOT_AVAILABLE;
}

\DDTrace\hook_method('App', 'init', $integration->setRootSpanInfoFn);
\DDTrace\hook_method('Dispatcher', '__construct', $integration->setRootSpanInfoFn);

\DDTrace\trace_method(
'Controller',
'invokeAction',
function (SpanData $span, array $args) use ($integration) {
$span->name = $span->resource = 'Controller.invokeAction';
$span->type = Type::WEB_SERVLET;
$span->service = $integration->appName;
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;

$request = $args[0];
if (!$request instanceof CakeRequest) {
return;
}

$rootSpan = \DDTrace\root_span();

if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) {
$rootSpan->resource =
$_SERVER['REQUEST_METHOD'] . ' ' . $this->name . 'Controller@' . $request->params['action'];
}

if (!array_key_exists(Tag::HTTP_URL, $rootSpan->meta)) {
$rootSpan->meta[Tag::HTTP_URL] = Router::url($request->here, true)
. Normalizer::sanitizedQueryString();
}
$rootSpan->meta['cakephp.route.controller'] = $request->params['controller'];
$rootSpan->meta['cakephp.route.action'] = $request->params['action'];
if (isset($request->params['plugin'])) {
$rootSpan->meta['cakephp.plugin'] = $request->params['plugin'];
}
}
);

// This only traces the default exception renderer
// Remove this when error tracking is added
// Other possible places to trace
// - ErrorHandler::handleException()
// - Controller::appError()
// - Exception.handler
// - Exception.renderer
\DDTrace\hook_method(
'ExceptionRenderer',
'__construct',
$integration->handleExceptionFn
);

\DDTrace\hook_method(
'CakeResponse',
'statusCode',
null,
$integration->setStatusCodeFn
);

// Create a trace span for every template rendered
\DDTrace\trace_method('View', 'render', function (SpanData $span) use ($integration) {
$span->name = 'cakephp.view';
$span->type = Type::WEB_SERVLET;
$file = $this->viewPath . '/' . $this->view . $this->ext;
$span->resource = $file;
$span->meta = ['cakephp.view' => $file];
$span->service = $integration->appName;
$span->meta[Tag::COMPONENT] = CakePHPIntegration::NAME;
});

\DDTrace\hook_method(
'CakeRoute',
'parse',
null,
$integration->parseRouteFn
);

return Integration::LOADED;
}
}
Loading

0 comments on commit bbd3a9c

Please sign in to comment.