Skip to content

Commit 7b4a640

Browse files
PROFeNoMbwoebi
andauthored
feat: Swoole Integration (#2595)
* Initial Test Setup * tests: Add test suite * tests: Run swoole tests * Revert stub changes * tests: Add Swoole 4 Test Suite * tests: Remove debug logs * Revert * Add a note for posterity * Change test name * fix: Only load for Swoole 5.0.2+ * Check `Response::header` arg count Co-authored-by: Bob Weinand <[email protected]> * Check for `Response::status` arg count Co-authored-by: Bob Weinand <[email protected]> * fix: Identify SSL Connections * Move `$scheme` before `install_hook` --------- Co-authored-by: Bob Weinand <[email protected]>
1 parent 3d4313f commit 7b4a640

17 files changed

Lines changed: 727 additions & 0 deletions

File tree

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,7 @@ TEST_INTEGRATIONS_80 := \
797797
test_integrations_pcntl \
798798
test_integrations_predis1 \
799799
test_integrations_sqlsrv \
800+
test_integrations_swoole_5 \
800801
test_opentracing_10
801802

802803
TEST_WEB_80 := \
@@ -843,6 +844,7 @@ TEST_INTEGRATIONS_81 := \
843844
test_integrations_elasticsearch7 \
844845
test_integrations_predis1 \
845846
test_integrations_sqlsrv \
847+
test_integrations_swoole_5 \
846848
test_opentracing_10
847849

848850
TEST_WEB_81 := \
@@ -891,6 +893,7 @@ TEST_INTEGRATIONS_82 := \
891893
test_integrations_predis1 \
892894
test_integrations_roadrunner \
893895
test_integrations_sqlsrv \
896+
test_integrations_swoole_5 \
894897
test_opentracing_10
895898

896899
TEST_WEB_82 := \
@@ -942,6 +945,7 @@ TEST_INTEGRATIONS_83 := \
942945
test_integrations_predis1 \
943946
test_integrations_roadrunner \
944947
test_integrations_sqlsrv \
948+
test_integrations_swoole_5 \
945949
test_opentracing_10
946950

947951
TEST_WEB_83 := \
@@ -1216,6 +1220,9 @@ test_integrations_roadrunner: global_test_run_dependencies
12161220
test_integrations_sqlsrv: global_test_run_dependencies
12171221
$(MAKE) test_scenario_default
12181222
$(call run_tests_debug,tests/Integrations/SQLSRV)
1223+
test_integrations_swoole_5: global_test_run_dependencies
1224+
$(MAKE) test_scenario_swoole5
1225+
$(call run_tests_debug,--testsuite=swoole-test)
12191226
test_web_cakephp_28: global_test_run_dependencies
12201227
$(call run_composer_with_retry,tests/Frameworks/CakePHP/Version_2_8,)
12211228
$(call run_tests_debug,--testsuite=cakephp-28-test)

bridge/_files_integrations.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
__DIR__ . '/../src/Integrations/Integrations/Mongo/MongoIntegration.php',
3131
__DIR__ . '/../src/Integrations/Integrations/MongoDB/MongoDBIntegration.php',
3232
__DIR__ . '/../src/Integrations/Integrations/Slim/SlimIntegration.php',
33+
__DIR__ . '/../src/Integrations/Integrations/Swoole/SwooleIntegration.php',
3334
__DIR__ . '/../src/Integrations/Integrations/SQLSRV/SQLSRVIntegration.php',
3435
__DIR__ . '/../src/Integrations/Integrations/Symfony/SymfonyIntegration.php',
3536
__DIR__ . '/../src/Integrations/Integrations/ElasticSearch/V1/ElasticSearchCommon.php',

ext/integrations/integrations.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@ void ddtrace_integrations_minit(void) {
311311
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_SLIM, "Slim\\App", "__construct",
312312
"DDTrace\\Integrations\\Slim\\SlimIntegration");
313313

314+
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_SWOOLE, "Swoole\\Http\\Server", "on",
315+
"DDTrace\\Integrations\\Swoole\\SwooleIntegration");
316+
314317
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_LARAVELQUEUE, "Illuminate\\Queue\\Worker", "__construct",
315318
"DDTrace\\Integrations\\LaravelQueue\\LaravelQueueIntegration");
316319
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_LARAVELQUEUE, "Illuminate\\Contracts\\Queue\\Queue", "push",

ext/integrations/integrations.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
INTEGRATION(ROADRUNNER, "roadrunner") \
3838
INTEGRATION(SQLSRV, "sqlsrv") \
3939
INTEGRATION(SLIM, "slim") \
40+
INTEGRATION(SWOOLE, "swoole") \
4041
INTEGRATION(SYMFONY, "symfony") \
4142
INTEGRATION(WEB, "web") \
4243
INTEGRATION(WORDPRESS, "wordpress") \
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
namespace DDTrace\Integrations\Swoole;
4+
5+
use DDTrace\HookData;
6+
use DDTrace\Integrations\Integration;
7+
use DDTrace\SpanStack;
8+
use DDTrace\Tag;
9+
use DDTrace\Type;
10+
use DDTrace\Util\Normalizer;
11+
use Swoole\Http\Request;
12+
use Swoole\Http\Response;
13+
use Swoole\Http\Server;
14+
use function DDTrace\consume_distributed_tracing_headers;
15+
use function DDTrace\extract_ip_from_headers;
16+
17+
class SwooleIntegration extends Integration
18+
{
19+
const NAME = 'swoole';
20+
21+
public function getName()
22+
{
23+
return self::NAME;
24+
}
25+
26+
/**
27+
* {@inheritdoc}
28+
*/
29+
public function requiresExplicitTraceAnalyticsEnabling()
30+
{
31+
return false;
32+
}
33+
34+
public function instrumentRequestStart(callable $callback, SwooleIntegration $integration, Server $server)
35+
{
36+
$scheme = $server->ssl ? 'https://' : 'http://';
37+
38+
\DDTrace\install_hook(
39+
$callback,
40+
function (HookData $hook) use ($integration, $server, $scheme) {
41+
$rootSpan = $hook->span(new SpanStack());
42+
$rootSpan->name = "web.request";
43+
$rootSpan->service = \ddtrace_config_app_name('swoole');
44+
$rootSpan->type = Type::WEB_SERVLET;
45+
$rootSpan->meta[Tag::COMPONENT] = SwooleIntegration::NAME;
46+
$rootSpan->meta[Tag::SPAN_KIND] = Tag::SPAN_KIND_VALUE_SERVER;
47+
$integration->addTraceAnalyticsIfEnabled($rootSpan);
48+
49+
$args = $hook->args;
50+
/** @var Request $request */
51+
$request = $args[0];
52+
53+
$headers = [];
54+
$allowedHeaders = \dd_trace_env_config('DD_TRACE_HEADER_TAGS');
55+
foreach ($request->header as $name => $value) {
56+
$headers[strtolower($name)] = $value;
57+
$normalizedHeader = preg_replace("([^a-z0-9-])", "_", strtolower($name));
58+
if (\array_key_exists($normalizedHeader, $allowedHeaders)) {
59+
$rootSpan->meta["http.request.headers.$normalizedHeader"] = $value;
60+
}
61+
}
62+
consume_distributed_tracing_headers(function ($key) use ($headers) {
63+
return $headers[$key] ?? null;
64+
});
65+
66+
if (\dd_trace_env_config("DD_TRACE_CLIENT_IP_ENABLED")) {
67+
$res = extract_ip_from_headers($headers + ['REMOTE_ADDR' => $request->server['remote_addr']]);
68+
$rootSpan->meta += $res;
69+
}
70+
71+
if (isset($headers["user-agent"])) {
72+
$rootSpan->meta["http.useragent"] = $headers["user-agent"];
73+
}
74+
75+
$rawContent = $request->rawContent();
76+
if ($rawContent) {
77+
// The raw content will always be populated if the request is a POST request, independent of the
78+
// Content-Type header.
79+
// However, it may not be json-decodable
80+
$postFields = json_decode($rawContent, true);
81+
if (is_null($postFields)) {
82+
// Fallback to the post fields, which is an array
83+
// This array is not always populated, depending on the Content-Type header
84+
$postFields = $request->post;
85+
}
86+
}
87+
if (!empty($postFields)) {
88+
$postFields = Normalizer::sanitizePostFields($postFields);
89+
foreach ($postFields as $key => $value) {
90+
$rootSpan->meta["http.request.post.$key"] = $value;
91+
}
92+
}
93+
94+
$normalizedPath = Normalizer::uriNormalizeincomingPath(
95+
$request->server['request_uri']
96+
?? $request->server['path_info']
97+
?? '/'
98+
);
99+
$rootSpan->resource = $request->server['request_method'] . ' ' . $normalizedPath;
100+
$rootSpan->meta[Tag::HTTP_METHOD] = $request->server['request_method'];
101+
102+
$host = $headers['host'] ?? ($request->server['remote_addr'] . ':' . $request->server['server_port']);
103+
$path = $request->server['request_uri'] ?? $request->server['path_info'] ?? '';
104+
$query = isset($request->server['query_string']) ? '?' . $request->server['query_string'] : '';
105+
$url = $scheme . $host . $path . $query;
106+
$rootSpan->meta[Tag::HTTP_URL] = Normalizer::uriNormalizeincomingPath($url);
107+
108+
unset($rootSpan->meta['closure.declaration']);
109+
}
110+
);
111+
}
112+
113+
public function init()
114+
{
115+
if (version_compare(swoole_version(), '5.0.2', '<')) {
116+
return Integration::NOT_LOADED;
117+
}
118+
119+
$integration = $this;
120+
121+
ini_set("datadog.trace.auto_flush_enabled", 1);
122+
ini_set("datadog.trace.generate_root_span", 0);
123+
124+
\DDTrace\hook_method(
125+
'Swoole\Http\Server',
126+
'on',
127+
null,
128+
function ($server, $scope, $args, $retval) use ($integration) {
129+
if ($retval === false) {
130+
return; // Callback wasn't set
131+
}
132+
133+
list($eventName, $callback) = $args;
134+
135+
if ($eventName === 'request') {
136+
$integration->instrumentRequestStart($callback, $integration, $server);
137+
}
138+
}
139+
);
140+
141+
\DDTrace\hook_method(
142+
'Swoole\Http\Response',
143+
'end',
144+
function ($response, $scope, $args) use ($integration) {
145+
$rootSpan = \DDTrace\root_span();
146+
if ($rootSpan === null) {
147+
return;
148+
}
149+
150+
// Note: The response's body can be retrieved here, from the args
151+
152+
if (!$rootSpan->exception
153+
&& ((int)$rootSpan->meta[Tag::HTTP_STATUS_CODE]) >= 500
154+
&& $ex = \DDTrace\find_active_exception()
155+
) {
156+
$rootSpan->exception = $ex;
157+
}
158+
}
159+
);
160+
161+
\DDTrace\hook_method(
162+
'Swoole\Http\Response',
163+
'header',
164+
function ($response, $scope, $args) use ($integration) {
165+
$rootSpan = \DDTrace\root_span();
166+
if ($rootSpan === null || \count($args) < 2) {
167+
return;
168+
}
169+
170+
/** @var string[] $args */
171+
list($key, $value) = $args;
172+
173+
$allowedHeaders = \dd_trace_env_config("DD_TRACE_HEADER_TAGS");
174+
$normalizedHeader = preg_replace("([^a-z0-9-])", "_", strtolower($key));
175+
if (\array_key_exists($normalizedHeader, $allowedHeaders)) {
176+
$rootSpan->meta["http.response.headers.$normalizedHeader"] = $value;
177+
}
178+
}
179+
);
180+
181+
\DDTrace\hook_method(
182+
'Swoole\Http\Response',
183+
'status',
184+
function ($response, $scope, $args) use ($integration) {
185+
$rootSpan = \DDTrace\root_span();
186+
if ($rootSpan && \count($args) > 0) {
187+
$rootSpan->meta[Tag::HTTP_STATUS_CODE] = $args[0];
188+
}
189+
}
190+
);
191+
192+
return Integration::LOADED;
193+
}
194+
}

tests/Common/WebFrameworkTestCase.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ protected static function getRoadrunnerVersion()
6767
return null;
6868
}
6969

70+
protected static function isSwoole()
71+
{
72+
return false;
73+
}
74+
7075
/**
7176
* Get additional envs to be set in the web server.
7277
* @return array
@@ -130,6 +135,9 @@ protected static function setUpWebServer(array $additionalEnvs = [], array $addi
130135
if ($version = static::getRoadrunnerVersion()) {
131136
self::$appServer->setRoadrunner($version);
132137
}
138+
if ($version = static::isSwoole()) {
139+
self::$appServer->setSwoole($version);
140+
}
133141
self::$appServer->start();
134142
}
135143
}

tests/Frameworks/Swoole/index.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
require __DIR__ . '/../../vendor/autoload.php';
4+
5+
$http = new Swoole\Http\Server("0.0.0.0", 9999);
6+
7+
$http->on('request', function ($request, $response) {
8+
$requestUri = $request->server['request_uri'];
9+
10+
try {
11+
if ($requestUri == "/error") {
12+
throw new \Exception("Error page");
13+
}
14+
15+
$response->status(200);
16+
$response->end('Hello Swoole!');
17+
} catch (\Throwable $e) {
18+
$response->status(500);
19+
$response->end('Something Went Wrong!');
20+
}
21+
});
22+
23+
$http->start();

0 commit comments

Comments
 (0)