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
202 changes: 202 additions & 0 deletions src/Integrations/Integrations/Swoole/SwooleIntegration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php

namespace DDTrace\Integrations\Swoole;

use DDTrace\HookData;
use DDTrace\Integrations\Integration;
use DDTrace\SpanData;
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;
}

public function addTraceAnalyticsIfEnabled(SpanData $span)
{
if (!$this->configuration->isTraceAnalyticsEnabled()) {
return;
}
$span->metrics[Tag::ANALYTICS_KEY] = $this->configuration->getTraceAnalyticsSampleRate();
}
PROFeNoM marked this conversation as resolved.
Show resolved Hide resolved

public function instrumentRequestStart(callable $callback, SwooleIntegration $integration)
{
\DDTrace\install_hook(
$callback,
function (HookData $hook) use (&$rootSpan, $integration) {
$rootSpan = \DDTrace\start_trace_span();
$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'];

$serverProtocol = $request->server['server_protocol'] ?? 'HTTP/1.1';
$scheme = strpos($serverProtocol, 'HTTPS') !== false ? 'https://' : 'http://';
Copy link
Collaborator

Choose a reason for hiding this comment

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

really? TLS should be independent from the protocol, unless that's something special swoole does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's wrong

Copy link
Contributor Author

@PROFeNoM PROFeNoM Mar 27, 2024

Choose a reason for hiding this comment

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

I changed this part to use the server's SSL property. Since I seemingly don't have access to the URL, I think that's the best way of infering the scheme

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, checking ssl vs non-ssl is the right way to do that.

$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);
},
function (HookData $hook) use (&$rootSpan, $integration) {
Copy link
Collaborator

@bwoebi bwoebi Mar 26, 2024

Choose a reason for hiding this comment

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

This doesn't look right to me. &$rootSpan will not necessarily be the root span the Request was started with - multiple requests may run in parallel with swoole.

In the begin hook, use $rootSpan = $hook->span(new \DDTrace\SpanStack);. Then you also don't have to manually close the span in the end hook. The span can be accessed in the end span with $hook->span() (but this is unnecessary, as it's now a hook span, which will inherit the exception anyway).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed 👍 That makes me wonder whether we should do the same for the RoadRunner integration 🤔

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 there too, but for roadrunner it's not a bug at least.

if ($hook->exception) {
$rootSpan->exception = $hook->exception;
}

\DDTrace\close_spans_until($rootSpan);
\DDTrace\close_span();
}
);
}

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);
}
}
);

\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) {
return;
}

/** @var string $key */
$key = $args[0];
/** @var string $value */
$value = $args[1];
PROFeNoM marked this conversation as resolved.
Show resolved Hide resolved

$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) {
PROFeNoM marked this conversation as resolved.
Show resolved Hide resolved
$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();