-
Notifications
You must be signed in to change notification settings - Fork 149
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
feat: Swoole Integration #2595
Changes from 13 commits
f6b2b9f
e56fc3f
738495c
bf27730
33e5f1d
75d6e5d
741b610
96dfe24
410f194
04b301a
6479abc
93a5470
c3c7673
312faa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
<?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) | ||
{ | ||
\DDTrace\install_hook( | ||
$callback, | ||
function (HookData $hook) use ($integration, $server) { | ||
$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']; | ||
|
||
$scheme = $server->ssl ? 'https://' : 'http://'; | ||
$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']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move that comparison before the install_hook call? and use ($scheme) instead? Apart from the minuscule micro-optimization, it also avoids having a circular dependency. Not sure if it makes any difference, but I think we should.