diff --git a/.github/workflows/build_images.yml b/.github/workflows/build_images.yml new file mode 100644 index 00000000000..2d71f372f7c --- /dev/null +++ b/.github/workflows/build_images.yml @@ -0,0 +1,42 @@ +on: [push] +jobs: + cancel: + runs-on: ubuntu-latest + name: Cancel Previous Runs + if: always() + steps: + - uses: styfle/cancel-workflow-action@d57d93c3a8110b00c3a2c0b64b8516013c9fd4c9 + if: github.ref != 'refs/heads/master' + name: cancel old workflows + id: cancel + with: + workflow_id: "build_images.yml" + access_token: ${{ github.token }} + - if: github.ref == 'refs/heads/master' + name: Don't cancel old workflows + id: dont_cancel + run: | + echo "Don't cancel old workflow" + build-web: + name: Extension Build + runs-on: ubuntu-latest + needs: [cancel] + continue-on-error: false + steps: + - name: Checkout repo + uses: actions/checkout@v2 + - name: Login to Dockerhub + uses: docker/login-action@v1 + with: + username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + - name: Push To Dockerhub + uses: docker/build-push-action@v1 + with: + username: ${{ secrets.PUBLIC_DOCKER_USERNAME }} + password: ${{ secrets.PUBLIC_DOCKER_PASSWORD }} + repository: ${{ github.repository }} + tags: ${{ github.sha }} + dockerfile: Dockerfile.rzp + build_args: GIT_COMMIT_HASH=${{ github.sha }},GIT_TOKEN=${{ secrets.GIT_ACTION_TOKEN }},GIT_USERNAME=rzp + diff --git a/Dockerfile.rzp b/Dockerfile.rzp new file mode 100644 index 00000000000..92563202276 --- /dev/null +++ b/Dockerfile.rzp @@ -0,0 +1,5 @@ +FROM razorpay/onggi:php-base + +COPY ext /ext + +RUN cd /ext && phpize && ./configure --enable-opencensus && make install diff --git a/ext/README.md b/ext/README.md index c14f0c55925..bc5b1ba8ca2 100644 --- a/ext/README.md +++ b/ext/README.md @@ -317,6 +317,26 @@ You can retrieve the version of this extension at runtime. function opencensus_trace_version(); ``` +```php +/** + * Retrieve the count of collected trace spans, currently in memory + * + * @return int + */ +function opencensus_trace_count(); +} +``` + +```php +/** + * Removes a span from the list. + * + * @param string $key + * + */ +function opencensus_trace_remove_span($key); +``` + This library follows [Semantic Versioning](http://semver.org/). Please note it is currently under active development. Any release versioned @@ -342,3 +362,11 @@ See [CONTRIBUTING](../CONTRIBUTING.md) for more information on how to get starte ## License Apache 2.0 - See [LICENSE](LICENSE) for more information. + +## Exporting span once limit is reached to stop memory from bloating + +Have added $spanBufferLimit variable in ExtensionTracer.php to control the maximun number +of spans that can be hold in memory at any time, when the limit is reached the tracer itself +export all the closed spans. + + diff --git a/ext/opencensus.c b/ext/opencensus.c index fa524bfed6a..f811801fb0a 100644 --- a/ext/opencensus.c +++ b/ext/opencensus.c @@ -54,6 +54,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_opencensus_trace_add_attribute, 0, 0, 2) ZEND_ARG_ARRAY_INFO(0, options, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_opencensus_trace_remove_span, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_opencensus_trace_add_annotation, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, description, IS_STRING, 0) ZEND_ARG_ARRAY_INFO(0, options, 0) @@ -90,12 +94,14 @@ static zend_function_entry opencensus_functions[] = { PHP_FE(opencensus_trace_function, arginfo_opencensus_trace_function) PHP_FE(opencensus_trace_method, arginfo_opencensus_trace_method) PHP_FE(opencensus_trace_list, NULL) + PHP_FE(opencensus_trace_count, NULL) PHP_FE(opencensus_trace_begin, arginfo_opencensus_trace_begin) PHP_FE(opencensus_trace_finish, NULL) PHP_FE(opencensus_trace_clear, NULL) PHP_FE(opencensus_trace_set_context, arginfo_opencensus_trace_set_context) PHP_FE(opencensus_trace_context, NULL) PHP_FE(opencensus_trace_add_attribute, arginfo_opencensus_trace_add_attribute) + PHP_FE(opencensus_trace_remove_span, arginfo_opencensus_trace_remove_span) PHP_FE(opencensus_trace_add_annotation, arginfo_opencensus_trace_add_annotation) PHP_FE(opencensus_trace_add_link, arginfo_opencensus_trace_add_link) PHP_FE(opencensus_trace_add_message_event, arginfo_opencensus_trace_add_message_event) diff --git a/ext/opencensus_trace.c b/ext/opencensus_trace.c index 9072e182284..c13985204a0 100644 --- a/ext/opencensus_trace.c +++ b/ext/opencensus_trace.c @@ -21,6 +21,7 @@ #include "standard/php_math.h" #include "standard/php_rand.h" + /** * True globals for storing the original zend_execute_ex and * zend_execute_internal function pointers @@ -28,6 +29,9 @@ static void (*opencensus_original_zend_execute_ex) (zend_execute_data *execute_data); static void (*opencensus_original_zend_execute_internal) (zend_execute_data *execute_data, zval *return_value); +// Global value to keep the current number of spans in memory +static int SPAN_COUNT = 0; + void opencensus_trace_ginit() { /** @@ -160,6 +164,27 @@ PHP_FUNCTION(opencensus_trace_add_attribute) RETURN_FALSE; } +/** + * Removes a span corresponding to key/span_id from hashtable + * @param string $key + * @return bool + */ +PHP_FUNCTION(opencensus_trace_remove_span) +{ + zend_string *k; + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "S", &k) == FAILURE) { + RETURN_FALSE; + } + + // deleting th span assosciated with the given span_id + // When inserting the sapn, we also pass the destructor function for span span_dtor, which gets called on zend_hash_del + if (zend_hash_del(OPENCENSUS_G(spans), k) != SUCCESS) { + RETURN_FALSE + } + + RETURN_TRUE; +} + /** * Add an annotation to the current trace span * @@ -338,7 +363,6 @@ static zend_string *generate_span_id() static opencensus_trace_span_t *opencensus_trace_begin(zend_string *name, zend_execute_data *execute_data, zend_string *span_id TSRMLS_DC) { opencensus_trace_span_t *span = opencensus_trace_span_alloc(); - zend_fetch_debug_backtrace(&span->stackTrace, 1, DEBUG_BACKTRACE_IGNORE_ARGS, 0); span->start = opencensus_now(); @@ -357,7 +381,7 @@ static opencensus_trace_span_t *opencensus_trace_begin(zend_string *name, zend_e /* add the span to the list of spans */ zend_hash_add_ptr(OPENCENSUS_G(spans), span->span_id, span); - + SPAN_COUNT++; return span; } @@ -465,6 +489,7 @@ void span_dtor(zval *zv) opencensus_trace_span_t *span = Z_PTR_P(zv); opencensus_trace_span_free(span); ZVAL_PTR_DTOR(zv); + SPAN_COUNT--; } /** @@ -553,6 +578,7 @@ PHP_FUNCTION(opencensus_trace_context) * opencensus_original_zend_execute_ex */ void opencensus_trace_execute_ex (zend_execute_data *execute_data TSRMLS_DC) { + zend_string *function_name = opencensus_trace_add_scope_name( EG(current_execute_data)->func->common.function_name, EG(current_execute_data)->func->common.scope @@ -747,3 +773,13 @@ PHP_FUNCTION(opencensus_trace_list) } ZEND_HASH_FOREACH_END(); } +/** + * Return the count of trace spans that have been collected for this + * request + * + * @return long + */ +PHP_FUNCTION(opencensus_trace_count) +{ + RETURN_LONG(SPAN_COUNT) +} diff --git a/ext/opencensus_trace.h b/ext/opencensus_trace.h index 730c0cc2e09..d0ea0e6ad34 100644 --- a/ext/opencensus_trace.h +++ b/ext/opencensus_trace.h @@ -28,12 +28,14 @@ PHP_FUNCTION(opencensus_trace_function); PHP_FUNCTION(opencensus_trace_method); PHP_FUNCTION(opencensus_trace_list); +PHP_FUNCTION(opencensus_trace_count); PHP_FUNCTION(opencensus_trace_begin); PHP_FUNCTION(opencensus_trace_finish); PHP_FUNCTION(opencensus_trace_clear); PHP_FUNCTION(opencensus_trace_set_context); PHP_FUNCTION(opencensus_trace_context); PHP_FUNCTION(opencensus_trace_add_attribute); +PHP_FUNCTION(opencensus_trace_remove_span); PHP_FUNCTION(opencensus_trace_add_annotation); PHP_FUNCTION(opencensus_trace_add_link); PHP_FUNCTION(opencensus_trace_add_message_event); diff --git a/ext/php-helper/DaemonFunctions.php b/ext/php-helper/DaemonFunctions.php index d4583f45b5d..af5ba804720 100644 --- a/ext/php-helper/DaemonFunctions.php +++ b/ext/php-helper/DaemonFunctions.php @@ -61,6 +61,16 @@ function opencensus_trace_list(): array { return []; } +/** + * Retrieve the count of collected trace spans, currently in memory + * + * @return int + */ +function opencensus_trace_count(): int { + return 0; +} + + /** * Clear the list of collected trace spans * @@ -102,6 +112,17 @@ function opencensus_trace_set_context($traceId, $parentSpanId = null): void { function opencensus_trace_add_attribute($key, $value, $options = []): void { } + +/** + * Removes a span from the list. + * + * @param string $key + * + */ +function opencensus_trace_remove_span($key): bool { + +} + /** * Add an annotation to a span * @param string $description diff --git a/ext/tests/span_count.phpt b/ext/tests/span_count.phpt new file mode 100644 index 00000000000..55a0b1cbfb1 --- /dev/null +++ b/ext/tests/span_count.phpt @@ -0,0 +1,13 @@ +--TEST-- +OpenCensus Trace: Test span count method +--FILE-- + 123]); +opencensus_trace_finish(); + +$count = opencensus_trace_count(); +echo "Number of traces: " . $count . "\n"; +?> +--EXPECTF-- +Number of traces: 1 diff --git a/ext/tests/span_remove.phpt b/ext/tests/span_remove.phpt new file mode 100644 index 00000000000..8fa9624ab6f --- /dev/null +++ b/ext/tests/span_remove.phpt @@ -0,0 +1,19 @@ +--TEST-- +OpenCensus Trace: Test removing span by id +--FILE-- + 123]); +opencensus_trace_finish(); + +$traces = opencensus_trace_list(); +echo "Number of traces: " . count($traces) . "\n"; +$span = $traces[0]; +$id = $span->spanId(); +opencensus_trace_remove_span($id); +$count = opencensus_trace_count(); +echo "Number of traces: " . $count . "\n"; +?> +--EXPECTF-- +Number of traces: 1 +Number of traces: 0 diff --git a/src/Trace/Integrations/Curl.php b/src/Trace/Integrations/Curl.php index 1f0abce4199..b38bbaa8277 100644 --- a/src/Trace/Integrations/Curl.php +++ b/src/Trace/Integrations/Curl.php @@ -18,6 +18,7 @@ namespace OpenCensus\Trace\Integrations; use OpenCensus\Trace\Span; +use OpenCensus\Trace\Tracer; /** * This class handles instrumenting curl requests using the opencensus extension. @@ -55,11 +56,46 @@ public static function load() */ public static function handleCurlResource($resource) { + $info = curl_getinfo($resource); + $attrs = self::getSpanAttrsFromCurlInfo($info); + + // checks if span limit has reached and if yes exports the closed spans + if (Tracer::$tracer != null) { + Tracer::$tracer->checkSpanLimit(); + } + return [ - 'attributes' => [ - 'uri' => curl_getinfo($resource, CURLINFO_EFFECTIVE_URL) - ], + 'attributes' => $attrs, 'kind' => Span::KIND_CLIENT ]; } + + private static function getSpanAttrsFromCurlInfo($curlInfo) + { + $tagNameCurlInfoMap = [ + 'network.client.ip' => 'local_ip', + 'network.client.port' => 'local_port', + 'network.destination.ip' => 'primary_ip', + 'network.destination.port' => 'primary_port', + 'network.bytes_read' => 'size_download', + 'network.bytes_written' => 'size_upload', + 'time_total_in_secs' => 'total_time', + 'time_to_connect_in_secs' => 'connect_time', + 'time_to_redirect_in_secs' => 'redirect_time', + 'time_to_namelookup_in_secs' => 'namelookup_time', + 'time_to_pretransfer_in_secs' => 'pretransfer_time', + 'time_to_starttransfer_in_secs' => 'starttransfer_time', + 'primary_ip' => 'primary_ip', + 'uri' => 'url' + ]; + + $attrs = []; + + foreach ($tagNameCurlInfoMap as $tagName => $curlInfoName) { + if (isset($curlInfo[$curlInfoName]) && !\trim($curlInfo[$curlInfoName]) !== '') { + $attrs[$tagName] = $curlInfo[$curlInfoName]; + } + } + return $attrs; + } } diff --git a/src/Trace/Integrations/PDO.php b/src/Trace/Integrations/PDO.php index dd846d3a624..95615f7b3de 100644 --- a/src/Trace/Integrations/PDO.php +++ b/src/Trace/Integrations/PDO.php @@ -18,6 +18,7 @@ namespace OpenCensus\Trace\Integrations; use OpenCensus\Trace\Span; +use OpenCensus\Trace\Tracer; /** * This class handles instrumenting PDO requests using the opencensus extension. @@ -31,16 +32,18 @@ */ class PDO implements IntegrationInterface { + static $db_host = ""; /** * Static method to add instrumentation to the PDO requests */ - public static function load() + public static function load($db_host="") { if (!extension_loaded('opencensus')) { trigger_error('opencensus extension required to load PDO integrations.', E_USER_WARNING); return; } + PDO::$db_host = $db_host; // public int PDO::exec(string $query) opencensus_trace_method('PDO', 'exec', [static::class, 'handleQuery']); @@ -70,9 +73,18 @@ public static function load() */ public static function handleQuery($pdo, $query) { + // checks if span limit has reached and if yes exports the closed spans + if (Tracer::$tracer != null) { + Tracer::$tracer->checkSpanLimit(); + } + return [ - 'attributes' => ['query' => $query], - 'kind' => Span::KIND_CLIENT + 'attributes' => [ + 'db.statement' => $query, + 'span.kind' => Span::KIND_CLIENT + ], + 'kind' => Span::KIND_CLIENT, + 'sameProcessAsParentSpan' => false ]; } @@ -86,9 +98,58 @@ public static function handleQuery($pdo, $query) */ public static function handleConnect($pdo, $dsn) { + // https://www.php.net/manual/en/ref.pdo-mysql.connection.php + // example $dsn: mysql:host=localhost;dbname=testdb + // example $dsn: mysql:unix_socket=/tmp/mysql.sock;dbname=testdb + + + // checks if span limit has reached and if yes exports the closed spans + if (Tracer::$tracer != null) { + Tracer::$tracer->checkSpanLimit(); + } + + $db_system = ''; + $connection_params = []; + $attributes = []; + + $dbtype_connection = explode(":", $dsn); + if (count($dbtype_connection) >= 2) { + $db_system = $dbtype_connection[0]; + $connection = $dbtype_connection[1]; + foreach (explode(";", $connection) as $kv) { + $params = explode("=", $kv); + $connection_params[$params[0]] = $params[1]; + } + } + + if ($db_system) { + $attributes['db.system'] = $db_system; + } + if (array_key_exists('dbname', $connection_params)) { + $attributes['db.name'] = $connection_params['dbname']; + } + if (array_key_exists('port', $connection_params)) { + $attributes['net.peer.port'] = $connection_params['port']; + } + + if (array_key_exists('host', $connection_params)) { + $attributes['net.peer.name'] = $connection_params['host']; + } elseif (array_key_exists('unix_socket', $connection_params)) { + $attributes['net.peer.name'] = PDO::$db_host; + } + + $attributes += [ + 'dsn' => $dsn, + 'db.type' => 'sql', + 'db.connection_string' => $dsn, + 'span.kind' => Span::KIND_CLIENT + ]; + return [ - 'attributes' => ['dsn' => $dsn], - 'kind' => Span::KIND_CLIENT + 'attributes' => $attributes, + 'kind' => Span::KIND_CLIENT, + 'sameProcessAsParentSpan' => false, + 'name' => 'PDO connect' ]; } @@ -101,9 +162,117 @@ public static function handleConnect($pdo, $dsn) */ public static function handleStatementExecute($statement) { + /* + refer following for SQL return codes + https://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm + */ + + // checks if span limit has reached and if yes flushes the closed spans + if (Tracer::$tracer != null) { + Tracer::$tracer->checkSpanLimit(); + } + + $rowCount = $statement->rowCount(); + $errorCode = $statement->errorCode(); + $error = substr($errorCode, 0, 2); + $errorTags = []; + + switch ($error) { + case (string) '01': + $errorTags = ['warning' => 'true', 'warning.code' => $errorCode]; + break; + }; + + $errorCodeMsgArray = [ + "02" => "No Data", + "07" => "Dynamic SQL error", + "08" => "Connection Exception", + "0A" => "Feature not supported", + "21" => "Cardinality violation", + "22" => "Data exception", + "23" => "Integrity constraint violation", + "24" => "Invalid Cursor State", + "25" => "Invalid Transaction state", + "26" => "Invalid SQL Statement Name", + "27" => "Triggered Data Change Violation", + "28" => "Invalid Authorization Specification", + "2A" => "Syntax Error or Access Rule Violation in Direct SQL Statement", + "2B" => "Dependent Privilege Descriptors Still Exist", + "2C" => "Invalid Character Set Name", + "2D" => "Invalid Transaction Termination", + "2E" => "Invalid Connection Name", + "33" => "Invalid SQL Descriptor Name", + "34" => "Invalid Cursor Name", + "35" => "Invalid Condition Number", + "37" => "Syntax Error or Access Rule Violation in Dynamic SQL Statement", + "3C" => "Ambigous Cursor Name", + "3F" => "No Data", + "40" => "Transition Rollback", + "42" => "Syntax Error or Access Rule Violation", + "44" => "With Check Option Violation" + ]; + + if (array_key_exists($error, $errorCodeMsgArray)) { + $errorTags['error'] = 'true'; + $errorTags['error.code'] = $errorCode; + $errorTags['error.message'] = $errorCodeMsgArray[$error] ?? ''; + } + + $query = $statement->queryString; + $operation = PDO::getOperationName($query); + $tableName = PDO::getTableName($query, $operation); + + $tags = [ + 'db.statement' => $query, + 'db.row_count' => $rowCount, + 'db.operation' => $operation, + 'db.table' => $tableName, + 'db.sql.table' => $tableName, + 'span.kind' => Span::KIND_CLIENT + ]; + return [ - 'attributes' => ['query' => $statement->queryString], - 'kind' => Span::KIND_CLIENT + 'attributes' => $tags + $errorTags, + 'kind' => Span::KIND_CLIENT, + 'sameProcessAsParentSpan' => false, + 'name' => sprintf("PDO %s %s", $operation, $tableName) ]; } + + public static function getOperationName($query){ + // select/insert/update/delete + + // some queries are enclosed in (). trim them before figuring out operation. + $operation = explode(" ", trim($query, "( "))[0]; + return $operation; + } + + public static function getTableName($query, $operation){ + $tableName = ""; + $operation = strtolower($operation); + $query = strtolower(trim($query)); + $query_parts = explode(" ", $query); + + if (($operation === 'select') or ($operation === 'delete')){ + // select <...> from where ... + // delete from where ... + $from_index = array_search('from', $query_parts); + if (($from_index) and ($from_index+1 < count($query_parts))){ + $tableName = $query_parts[$from_index+1]; + } + } + else if (strtolower($operation) === 'update'){ + // update set ... where ... + $tableName = $query_parts[1]; + } + else if (strtolower($operation) === 'insert'){ + // insert into ... + $into_index = array_search('into', $query_parts); + if (($into_index) and ($into_index+1 < count($query_parts))){ + $tableName = $query_parts[$into_index+1]; + } + } + + return trim($tableName, " \n\r\t\v\0`"); + } } diff --git a/src/Trace/Integrations/Redis.php b/src/Trace/Integrations/Redis.php new file mode 100644 index 00000000000..752c4309b1a --- /dev/null +++ b/src/Trace/Integrations/Redis.php @@ -0,0 +1,135 @@ +checkSpanLimit(); + } + $connection_str = sprintf("%s:%s", $params[0]['host'], $params[0]['port']); + return [ + 'attributes' => [ + 'peer.hostname' => $params[0]['host'], + 'peer.port' => $params[0]['port'], + 'net.peer.name' => $params[0]['host'], + 'db.type' => 'redis', + 'db.system' => 'redis', + 'db.connection_string' => $connection_str, + 'span.kind' => Span::KIND_CLIENT + ], + 'kind' => Span::KIND_CLIENT, + 'name' => 'Predis connect', + 'sameProcessAsParentSpan' => false + ]; + }); + + // covers all basic commands + opencensus_trace_method('Predis\Client', 'executeCommand', function ($predis, $command) { + $arguments = $command->getArguments(); + array_unshift($arguments, $command->getId()); + $query = Redis::formatArguments($arguments); + + // checks if spanlimit has reached and if yes flushes the closed spans + if (Tracer::$tracer != null) { + Tracer::$tracer->checkSpanLimit(); + } + + return ['attributes' => [ + 'db.type' => 'redis', + 'db.system' => 'redis', + 'db.statement' => $query, + 'db.operation' => $command->getId(), + 'command' => $command->getId(), + 'service.name' => 'redis', + 'redis.raw_command' => $query, + 'redis.args_length' => count($arguments), + 'span.kind' => Span::KIND_CLIENT + ], + 'kind' => Span::KIND_CLIENT, + 'name' => 'Predis ' . $command->getId(), + 'sameProcessAsParentSpan' => false + ]; + }); + } + + public static function formatArguments($arguments) + { + $len = 0; + $out = []; + + foreach ($arguments as $argument) { + // crude test to skip binary + if (strpos($argument, "\0") !== false) { + continue; + } + + $cmd = (string)$argument; + + if (strlen($cmd) > VALUE_MAX_LEN) { + $cmd = substr($cmd, 0, VALUE_MAX_LEN) . VALUE_TOO_LONG_MARK; + } + + if (($len + strlen($cmd)) > CMD_MAX_LEN) { + $prefix = substr($cmd, 0, CMD_MAX_LEN - $len); + $out[] = $prefix . VALUE_TOO_LONG_MARK; + break; + } + + $out[] = $cmd; + $len += strlen($cmd); + } + + return implode(' ', $out); + } +} diff --git a/src/Trace/Propagator/JaegerPropagator.php b/src/Trace/Propagator/JaegerPropagator.php new file mode 100644 index 00000000000..a5b1af0f5e4 --- /dev/null +++ b/src/Trace/Propagator/JaegerPropagator.php @@ -0,0 +1,105 @@ +header = $header; + } + + public function extract(HeaderGetter $headers): SpanContext + { + // normalize header name that comes in, like php does it + $extract_header = 'HTTP_' . strtoupper(str_replace('-', '_', $this->header)); + + $data = $headers->get($extract_header); + + if (!$data) { + return new SpanContext(); + } + + // Jaeger trace id can be of length either 16 or 32. (https://www.jaegertracing.io/docs/1.21/client-libraries/#value) + // We have decided to continue with trace id of length 32 for injection. While extraction can accept both length 16 and 32. + $data = explode(":", $data); + if (count($data) < 4) { + return new SpanContext(); + } + + $traceId = $data[0]; + $spanId = $data[1]; + $parentSpanId = $data[2]; + $flags = $data[3]; + + $enabled = $flags & 0x01; + + $fromHeader = true; + + return new SpanContext($traceId, $spanId, $enabled, $fromHeader); + } + + public function inject(SpanContext $context, HeaderSetter $setter) + { + $traceId = $context->traceId(); + $spanId = $context->spanId(); + $parentID = ''; // this is deprecated anyway + $enabled = $context->enabled(); + + $value = sprintf(self::CONTEXT_HEADER_FORMAT, $traceId, $spanId, $parentID, $enabled); + + if (!headers_sent()) { + header("$this->header: $value"); + } + $setter->set($this->header, $value); + } +} diff --git a/src/Trace/RequestHandler.php b/src/Trace/RequestHandler.php index 2c83426e981..42225d18522 100644 --- a/src/Trace/RequestHandler.php +++ b/src/Trace/RequestHandler.php @@ -90,6 +90,7 @@ public function __construct( array $options = [] ) { $this->exporter = $exporter; + $this->propagator = $propagator; $this->headers = new ArrayHeaders($options['headers'] ?? $_SERVER); $spanContext = $propagator->extract($this->headers); @@ -107,16 +108,20 @@ public function __construct( if ($spanContext->enabled()) { $this->tracer = extension_loaded('opencensus') ? - new ExtensionTracer($spanContext) : + new ExtensionTracer($spanContext, $exporter, $options) : new ContextTracer($spanContext); } else { $this->tracer = new NullTracer(); } + $rootSpanName = $this->nameFromOptions($options) ?? $this->nameFromHeaders($this->headers->toArray()); + $rootSpanAttrs = $this->spanAttrsFromOptions($options); + unset($options['root_span_options']); + $spanOptions = $options + [ 'startTime' => $this->startTimeFromHeaders($this->headers->toArray()), - 'name' => $this->nameFromHeaders($this->headers->toArray()), - 'attributes' => [], + 'name' => $rootSpanName, + 'attributes' => $rootSpanAttrs, 'kind' => Span::KIND_SERVER, 'sameProcessAsParentSpan' => false ]; @@ -268,7 +273,13 @@ private function addCommonRequestAttributes(array $headers) $this->tracer->addAttribute(Span::ATTRIBUTE_STATUS_CODE, $responseCode, [ 'spanId' => $this->rootSpan->spanId() ]); + if ($responseCode >= 400) { + $this->tracer->addAttribute('error', 'true', [ + 'spanId' => $this->rootSpan->spanId() + ]); + } } + foreach (self::ATTRIBUTE_MAP as $attributeKey => $headerKeys) { if ($val = $this->detectKey($headerKeys, $headers)) { $this->tracer->addAttribute($attributeKey, $val, [ @@ -276,6 +287,23 @@ private function addCommonRequestAttributes(array $headers) ]); } } + + if (array_key_exists('QUERY_STRING', $headers)) { + // add all query parameters as tags + parse_str($headers['QUERY_STRING'], $queryParams); + + foreach ($queryParams as $key => $value) { + if (is_array($value)) { + $value = implode(', ', $value); + } + + $key = 'http.query.params.' . $key; + + $this->tracer->addAttribute($key, $value, [ + 'spanId' => $this->rootSpan->spanId() + ]); + } + } } private function startTimeFromHeaders(array $headers) @@ -289,9 +317,31 @@ private function startTimeFromHeaders(array $headers) return null; } + private function nameFromOptions(array $options) + { + $rootSpanOptions = array_key_exists('root_span_options', $options) + ? $options['root_span_options'] + : array(); + + return array_key_exists('name', $rootSpanOptions) ? $rootSpanOptions['name'] : null; + } + + private function spanAttrsFromOptions(array $options): array + { + $rootSpanOptions = array_key_exists('root_span_options', $options) + ? $options['root_span_options'] + : array(); + return array_key_exists('attributes', $rootSpanOptions) ? $rootSpanOptions['attributes'] : array(); + } + private function nameFromHeaders(array $headers): string { - return $headers['REQUEST_URI'] ?? self::DEFAULT_ROOT_SPAN_NAME; + // omit query parameters in the span name + if (array_key_exists('REQUEST_URI', $headers) and ($headers['REQUEST_URI'])) { + return strtok($headers['REQUEST_URI'], '?'); + } else { + return self::DEFAULT_ROOT_SPAN_NAME; + } } private function detectKey(array $keys, array $array) @@ -303,4 +353,9 @@ private function detectKey(array $keys, array $array) } return null; } + + public function inject(SpanContext $context, ArrayHeaders $headers) + { + $this->propagator->inject($context, $headers); + } } diff --git a/src/Trace/Tracer.php b/src/Trace/Tracer.php index 3c05ddb24d8..06ac236235a 100644 --- a/src/Trace/Tracer.php +++ b/src/Trace/Tracer.php @@ -23,6 +23,7 @@ use OpenCensus\Trace\Exporter\ExporterInterface; use OpenCensus\Trace\Propagator\PropagatorInterface; use OpenCensus\Trace\Propagator\HttpHeaderPropagator; +use OpenCensus\Trace\Propagator\ArrayHeaders; /** * This class provides static functions to give you access to the current @@ -100,6 +101,12 @@ class Tracer */ private static $instance; + /** + * @var RequestHandler Singleton instance + */ + public static $tracer; + + /** * Start a new trace session for this request. You should call this as early as * possible for the most accurate results. @@ -128,7 +135,11 @@ public static function start(ExporterInterface $reporter, array $options = []): : new HttpHeaderPropagator(); unset($options['propagator']); - return self::$instance = new RequestHandler($reporter, $sampler, $propagator, $options); + self::$instance = new RequestHandler($reporter, $sampler, $propagator, $options); + + self::$tracer = self::$instance->tracer(); + + return self::$instance; } /** @@ -301,4 +312,13 @@ public static function spanContext(): SpanContext { return isset(self::$instance) ? self::$instance->tracer()->spanContext() : new SpanContext(null, null, false); } + + public static function injectContext(ArrayHeaders $headers) + { + $context = self::spanContext(); + + if (isset(self::$instance)) { + self::$instance->inject($context, $headers); + } + } } diff --git a/src/Trace/Tracer/ExtensionTracer.php b/src/Trace/Tracer/ExtensionTracer.php index 42c45fddc6e..ed86a546168 100644 --- a/src/Trace/Tracer/ExtensionTracer.php +++ b/src/Trace/Tracer/ExtensionTracer.php @@ -42,16 +42,36 @@ class ExtensionTracer implements TracerInterface, SpanEventHandlerInterface */ private $hasSpans = false; + private $exporter; + + /** + * @var int + * Number of max spans that can be hold in a memory, if number goes beyond this value, + * tracer will export the closed spans till then. + */ + private $spanBufferLimit = 100; + /** * Create a new ExtensionTracer * * @param SpanContext|null $initialContext The starting span context. + * @param null $exporter. + * @param array $options + * @type int span_buffer_limit, overrides the default span buffer limit */ - public function __construct(SpanContext $initialContext = null) + public function __construct(SpanContext $initialContext = null, $exporter = null, $options = []) { if ($initialContext) { opencensus_trace_set_context($initialContext->traceId(), $initialContext->spanId()); } + + $this->exporter = $exporter; + + // set span limit from options if present + if (isset($options['span_buffer_limit'])){ + $this->spanBufferLimit = $options['span_buffer_limit']; + } + } public function inSpan(array $spanOptions, callable $callable, array $arguments = []) @@ -69,6 +89,9 @@ public function inSpan(array $spanOptions, callable $callable, array $arguments public function startSpan(array $spanOptions): Span { + // checks if spanlimit has reached and if yes flushes the closed spans + $this->checkSpanLimit(); + if (!array_key_exists('name', $spanOptions)) { $spanOptions['name'] = $this->generateSpanName(); } @@ -114,6 +137,41 @@ public function spans(): array }, opencensus_trace_list()); } + /* This checks the numbet of spans in memory and if the count is more than the set limit, it exports all + the closed span present in memory, to free up the memory. We are only exporting closed spans as only those spans use is over, + the open ones stop time along with other attributes might not have been set yet.*/ + public function checkSpanLimit() + { + $count = opencensus_trace_count(); + + if ($count >= $this->spanBufferLimit) { + + $closedSpans = []; + $spans = $this->spans(); + + foreach ($spans as $k) { + $endTime = $k->endTime(); + + if ($endTime->getTimestamp() != 0) { + $closedSpans[] = $k; + } + } + + $this->exportAndDeleteSpans($closedSpans); + } + } + + // Exports all the span provided as argument and also remove from memory + public function exportAndDeleteSpans($closedSpans) + { + if ($this->exporter != null) { + $this->exporter->export($closedSpans); + foreach ($closedSpans as $span) { + opencensus_trace_remove_span($span->spanId()); + } + } + } + public function addAttribute($attribute, $value, $options = []) { if (array_key_exists('span', $options)) { diff --git a/tests/unit/Trace/Integrations/CurlTest.php b/tests/unit/Trace/Integrations/CurlTest.php index ec189522d1f..51a2d5e72e0 100644 --- a/tests/unit/Trace/Integrations/CurlTest.php +++ b/tests/unit/Trace/Integrations/CurlTest.php @@ -31,13 +31,10 @@ public function testLoadUrlFromResource() $resource = curl_init('https://www.google.com'); $spanOptions = Curl::handleCurlResource($resource); - $expected = [ - 'attributes' => [ - 'uri' => 'https://www.google.com' - ], - 'kind' => Span::KIND_CLIENT - ]; + $expected_uri = 'https://www.google.com'; + $expected_kind = Span::KIND_CLIENT; - $this->assertEquals($expected, $spanOptions); + $this->assertEquals($expected_uri, $spanOptions['attributes']['uri']); + $this->assertEquals($expected_kind, $spanOptions['kind']); } } diff --git a/tests/unit/Trace/Integrations/PDOTest.php b/tests/unit/Trace/Integrations/PDOTest.php index d79625e8f87..0ac032c8a90 100644 --- a/tests/unit/Trace/Integrations/PDOTest.php +++ b/tests/unit/Trace/Integrations/PDOTest.php @@ -34,7 +34,7 @@ public function testHandleQuery() $spanOptions = PDO::handleQuery($scope, $query); $expected = [ 'attributes' => [ - 'query' => 'select * from users' + 'db.statement' => 'select * from users' ], 'kind' => Span::KIND_CLIENT ]; @@ -48,7 +48,8 @@ public function testHandleConnect() $spanOptions = PDO::handleConnect(null, $dsn); $expected = [ 'attributes' => [ - 'dsn' => 'mysql:host=localhost;dbname=testdb' + 'dsn' => 'mysql:host=localhost;dbname=testdb', + 'db.type' => 'sql' ], 'kind' => Span::KIND_CLIENT ];