Skip to content
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

feat: Swoole Integration #2595

Merged
merged 14 commits into from
Mar 28, 2024
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ TEST_INTEGRATIONS_80 := \
test_integrations_pcntl \
test_integrations_predis1 \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
test_opentracing_10

TEST_WEB_80 := \
Expand Down Expand Up @@ -843,6 +844,7 @@ TEST_INTEGRATIONS_81 := \
test_integrations_elasticsearch7 \
test_integrations_predis1 \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
test_opentracing_10

TEST_WEB_81 := \
Expand Down Expand Up @@ -891,6 +893,7 @@ TEST_INTEGRATIONS_82 := \
test_integrations_predis1 \
test_integrations_roadrunner \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
test_opentracing_10

TEST_WEB_82 := \
Expand Down Expand Up @@ -942,6 +945,7 @@ TEST_INTEGRATIONS_83 := \
test_integrations_predis1 \
test_integrations_roadrunner \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
test_opentracing_10

TEST_WEB_83 := \
Expand Down Expand Up @@ -1216,6 +1220,9 @@ test_integrations_roadrunner: global_test_run_dependencies
test_integrations_sqlsrv: global_test_run_dependencies
$(MAKE) test_scenario_default
$(call run_tests_debug,tests/Integrations/SQLSRV)
test_integrations_swoole_5: global_test_run_dependencies
$(MAKE) test_scenario_swoole5
$(call run_tests_debug,--testsuite=swoole-test)
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)
Expand Down
1 change: 1 addition & 0 deletions bridge/_files_integrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
__DIR__ . '/../src/Integrations/Integrations/Mongo/MongoIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/MongoDB/MongoDBIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Slim/SlimIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Swoole/SwooleIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/SQLSRV/SQLSRVIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Symfony/SymfonyIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/ElasticSearch/V1/ElasticSearchCommon.php',
Expand Down
3 changes: 3 additions & 0 deletions ext/integrations/integrations.c
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ void ddtrace_integrations_minit(void) {
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_SLIM, "Slim\\App", "__construct",
"DDTrace\\Integrations\\Slim\\SlimIntegration");

DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_SWOOLE, "Swoole\\Http\\Server", "on",
"DDTrace\\Integrations\\Swoole\\SwooleIntegration");

DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_LARAVELQUEUE, "Illuminate\\Queue\\Worker", "__construct",
"DDTrace\\Integrations\\LaravelQueue\\LaravelQueueIntegration");
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_LARAVELQUEUE, "Illuminate\\Contracts\\Queue\\Queue", "push",
Expand Down
1 change: 1 addition & 0 deletions ext/integrations/integrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
INTEGRATION(ROADRUNNER, "roadrunner") \
INTEGRATION(SQLSRV, "sqlsrv") \
INTEGRATION(SLIM, "slim") \
INTEGRATION(SWOOLE, "swoole") \
INTEGRATION(SYMFONY, "symfony") \
INTEGRATION(WEB, "web") \
INTEGRATION(WORDPRESS, "wordpress") \
Expand Down
194 changes: 194 additions & 0 deletions src/Integrations/Integrations/Swoole/SwooleIntegration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php

namespace DDTrace\Integrations\Swoole;

use DDTrace\HookData;
use DDTrace\Integrations\Integration;
use DDTrace\SpanStack;
use DDTrace\Tag;
use DDTrace\Type;
use DDTrace\Util\Normalizer;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Http\Server;
use function DDTrace\consume_distributed_tracing_headers;
use function DDTrace\extract_ip_from_headers;

class SwooleIntegration extends Integration
{
const NAME = 'swoole';

public function getName()
{
return self::NAME;
}

/**
* {@inheritdoc}
*/
public function requiresExplicitTraceAnalyticsEnabling()
{
return false;
}

public function instrumentRequestStart(callable $callback, SwooleIntegration $integration, Server $server)
{
$scheme = $server->ssl ? 'https://' : 'http://';

\DDTrace\install_hook(
$callback,
function (HookData $hook) use ($integration, $server, $scheme) {
$rootSpan = $hook->span(new SpanStack());
$rootSpan->name = "web.request";
$rootSpan->service = \ddtrace_config_app_name('swoole');
$rootSpan->type = Type::WEB_SERVLET;
$rootSpan->meta[Tag::COMPONENT] = SwooleIntegration::NAME;
$rootSpan->meta[Tag::SPAN_KIND] = Tag::SPAN_KIND_VALUE_SERVER;
$integration->addTraceAnalyticsIfEnabled($rootSpan);

$args = $hook->args;
/** @var Request $request */
$request = $args[0];

$headers = [];
$allowedHeaders = \dd_trace_env_config('DD_TRACE_HEADER_TAGS');
foreach ($request->header as $name => $value) {
$headers[strtolower($name)] = $value;
$normalizedHeader = preg_replace("([^a-z0-9-])", "_", strtolower($name));
if (\array_key_exists($normalizedHeader, $allowedHeaders)) {
$rootSpan->meta["http.request.headers.$normalizedHeader"] = $value;
}
}
consume_distributed_tracing_headers(function ($key) use ($headers) {
return $headers[$key] ?? null;
});

if (\dd_trace_env_config("DD_TRACE_CLIENT_IP_ENABLED")) {
$res = extract_ip_from_headers($headers + ['REMOTE_ADDR' => $request->server['remote_addr']]);
$rootSpan->meta += $res;
}

if (isset($headers["user-agent"])) {
$rootSpan->meta["http.useragent"] = $headers["user-agent"];
}

$rawContent = $request->rawContent();
if ($rawContent) {
// The raw content will always be populated if the request is a POST request, independent of the
// Content-Type header.
// However, it may not be json-decodable
$postFields = json_decode($rawContent, true);
if (is_null($postFields)) {
// Fallback to the post fields, which is an array
// This array is not always populated, depending on the Content-Type header
$postFields = $request->post;
}
}
if (!empty($postFields)) {
$postFields = Normalizer::sanitizePostFields($postFields);
foreach ($postFields as $key => $value) {
$rootSpan->meta["http.request.post.$key"] = $value;
}
}

$normalizedPath = Normalizer::uriNormalizeincomingPath(
$request->server['request_uri']
?? $request->server['path_info']
?? '/'
);
$rootSpan->resource = $request->server['request_method'] . ' ' . $normalizedPath;
$rootSpan->meta[Tag::HTTP_METHOD] = $request->server['request_method'];

$host = $headers['host'] ?? ($request->server['remote_addr'] . ':' . $request->server['server_port']);
$path = $request->server['request_uri'] ?? $request->server['path_info'] ?? '';
$query = isset($request->server['query_string']) ? '?' . $request->server['query_string'] : '';
$url = $scheme . $host . $path . $query;
$rootSpan->meta[Tag::HTTP_URL] = Normalizer::uriNormalizeincomingPath($url);

unset($rootSpan->meta['closure.declaration']);
Copy link
Collaborator

@bwoebi bwoebi Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are tracing the callback, we would always have a redundant, non-really-useful tag with the location of this closure's declaration. In the case of Laravel Octane, for instance, we would always have this tag pointing to swoole-server.php, which is not really helpful.

Additionally, but to a lesser extent, this is inconsistent with other frameworks/libraries. If we are setting the location of the request callback declaration from Swoole, why aren't we setting the same for Laravel and Kernel::handle, for instance?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

}
);
}

public function init()
{
if (version_compare(swoole_version(), '5.0.2', '<')) {
return Integration::NOT_LOADED;
}

$integration = $this;

ini_set("datadog.trace.auto_flush_enabled", 1);
ini_set("datadog.trace.generate_root_span", 0);

\DDTrace\hook_method(
'Swoole\Http\Server',
'on',
null,
function ($server, $scope, $args, $retval) use ($integration) {
if ($retval === false) {
return; // Callback wasn't set
}

list($eventName, $callback) = $args;

if ($eventName === 'request') {
$integration->instrumentRequestStart($callback, $integration, $server);
}
}
);

\DDTrace\hook_method(
'Swoole\Http\Response',
'end',
function ($response, $scope, $args) use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan === null) {
return;
}

// Note: The response's body can be retrieved here, from the args

if (!$rootSpan->exception
&& ((int)$rootSpan->meta[Tag::HTTP_STATUS_CODE]) >= 500
&& $ex = \DDTrace\find_active_exception()
) {
$rootSpan->exception = $ex;
}
}
);

\DDTrace\hook_method(
'Swoole\Http\Response',
'header',
function ($response, $scope, $args) use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan === null || \count($args) < 2) {
return;
}

/** @var string[] $args */
list($key, $value) = $args;

$allowedHeaders = \dd_trace_env_config("DD_TRACE_HEADER_TAGS");
$normalizedHeader = preg_replace("([^a-z0-9-])", "_", strtolower($key));
if (\array_key_exists($normalizedHeader, $allowedHeaders)) {
$rootSpan->meta["http.response.headers.$normalizedHeader"] = $value;
}
}
);

\DDTrace\hook_method(
'Swoole\Http\Response',
'status',
function ($response, $scope, $args) use ($integration) {
$rootSpan = \DDTrace\root_span();
if ($rootSpan && \count($args) > 0) {
$rootSpan->meta[Tag::HTTP_STATUS_CODE] = $args[0];
}
}
);

return Integration::LOADED;
}
}
8 changes: 8 additions & 0 deletions tests/Common/WebFrameworkTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ protected static function getRoadrunnerVersion()
return null;
}

protected static function isSwoole()
{
return false;
}

/**
* Get additional envs to be set in the web server.
* @return array
Expand Down Expand Up @@ -130,6 +135,9 @@ protected static function setUpWebServer(array $additionalEnvs = [], array $addi
if ($version = static::getRoadrunnerVersion()) {
self::$appServer->setRoadrunner($version);
}
if ($version = static::isSwoole()) {
self::$appServer->setSwoole($version);
}
self::$appServer->start();
}
}
Expand Down
23 changes: 23 additions & 0 deletions tests/Frameworks/Swoole/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

require __DIR__ . '/../../vendor/autoload.php';

$http = new Swoole\Http\Server("0.0.0.0", 9999);

$http->on('request', function ($request, $response) {
$requestUri = $request->server['request_uri'];

try {
if ($requestUri == "/error") {
throw new \Exception("Error page");
}

$response->status(200);
$response->end('Hello Swoole!');
} catch (\Throwable $e) {
$response->status(500);
$response->end('Something Went Wrong!');
}
});

$http->start();