dd-trace-php icon indicating copy to clipboard operation
dd-trace-php copied to clipboard

[Feature] Support Grpc-Php extension with distributed tracing

Open Bibob7 opened this issue 4 years ago • 9 comments

Is your feature request related to a problem? Please describe. We would like to have full support for this extension: https://github.com/grpc/grpc-php with distributed tracing.

Describe the solution you'd like When sending an receiving grpc requests with https://github.com/grpc/grpc-php, I would like to be able to see distributed traces.

Describe alternatives you've considered We already implemented a tracing integration on php site, which works fine apart from distributed tracing. (see: #915) However, for the future it would be fine, if there is full support for this extension.

Bibob7 avatar Jun 09 '20 06:06 Bibob7

For our use case, this would allow us to have spans for calls to GCP services as it makes use of GRPC.

nesl247 avatar Jul 23 '20 16:07 nesl247

Same here, most of our internal calls happen over gRPC, and currently we're blind on PHP apps.

CAFxX avatar Oct 20 '20 08:10 CAFxX

We do not have enough bandwidth to work on this integration during this quarter. Things might change if anyone of the other big challenges we are currently working on (support for PHP-8, adding profiling, stability/performance) will leave us some extra time. I just added the label up-for-grabs, in case anyone from the community is interested.

labbati avatar Oct 20 '20 13:10 labbati

Leaving this here for folks who might find this issue. I have a limited use case for tracing gRPC at the moment and only care about simple requests / unary calls. Was able to get things nicely into DD with the following snippet:

\dd_trace_method(BaseStub::class, '_simpleRequest', function ($spanData, $args, $returnValue, $exception = null) {
    $method = $args[0];
    $name = array_values(array_filter(explode('/', $method)))[0];
    // Not great to have to do this, but without that, there's just no info available during the `wait` call
    // so that's what it is for now
    $returnValue->__dd_trace_info = [
        'name' => $name,
        'resource' => $args[0],
    ];
    $spanData->name = $returnValue->__dd_trace_info['name'];
    $spanData->resource = 'init:'.$returnValue->__dd_trace_info['resource'];
    $spanData->service = 'grpc';

});

\dd_trace_method(UnaryCall::class, 'wait', function ($spanData, $args, $returnValue, $exception = null) {
    $spanData->name = $this->__dd_trace_info['name'] ?? '__grpc_name';
    $spanData->resource = 'wait:'.$this->__dd_trace_info['resource'] ?? '__grpc_resource';
    $spanData->service = 'grpc';
});

Screen Shot 2021-03-05 at 3 08 20 PM

khepin avatar Mar 05 '21 23:03 khepin

@khepin: Thanks for your snipped, but also in this case, you will have no distributed tracing. For that we need to be able to exchange span and trace id via grpc metadata.

Bibob7 avatar Apr 12 '21 06:04 Bibob7

@labbati: Are there any plans to work on that in the current or next quarter? For us this is still a relevant topic.

Bibob7 avatar Apr 12 '21 06:04 Bibob7

Hello @Bibob7, at the moment this has not been scheduled for Q2 and I have not an ETA I can commit to.

labbati avatar Apr 12 '21 09:04 labbati

We have achieved distributed tracing with the following:

use DDTrace\GlobalTracer;
use DDTrace\Tag;
use Grpc\Interceptor;

class DatadogGrpcInterceptor extends Interceptor
{
    const DEFAULT_BAGGAGE_HEADER_PREFIX = 'ot-baggage-';
    const DEFAULT_TRACE_ID_HEADER = 'x-datadog-trace-id';
    const DEFAULT_PARENT_ID_HEADER = 'x-datadog-parent-id';
    const DEFAULT_SAMPLING_PRIORITY_HEADER = 'x-datadog-sampling-priority';
    const DEFAULT_ORIGIN_HEADER = 'x-datadog-origin';

    public function interceptUnaryUnary($method, $argument, $deserialize, array $metadata = [], array $options = [], $continuation) // phpcs:ignore
    {
        $scope = GlobalTracer::get()->startActiveSpan("interceptUnaryUnary");
        $scope->getSpan()->setTag(Tag::SERVICE_NAME, "grpc-outbound");
        $scope->getSpan()->setTag(Tag::RESOURCE_NAME, $method);

        $spanContext = $scope->getSpan()->getContext();
        if ($spanContext->getTraceId() !== null) {
            $metadata[self::DEFAULT_TRACE_ID_HEADER] = array($spanContext->getTraceId());
        }
        if ($spanContext->getSpanId() !== null) {
            $metadata[self::DEFAULT_PARENT_ID_HEADER] = array($spanContext->getSpanId());
        }

        foreach ($spanContext as $key => $value) {
            if ($value !== null) {
                $metadata[self::DEFAULT_BAGGAGE_HEADER_PREFIX . $key] = array($value);
            }
        }

        $prioritySampling = GlobalTracer::get()->getPrioritySampling();
        if ($prioritySampling !== null) {
            $metadata[self::DEFAULT_SAMPLING_PRIORITY_HEADER] = array(strval($prioritySampling));
        }
        if (!empty($spanContext->origin)) {
            $metadata[self::DEFAULT_ORIGIN_HEADER] = array($spanContext->origin);
        }

        try {
            return $continuation($method, $argument, $deserialize, $metadata, $options);
        } finally {
            $scope->close();
        }
    }
}

And in the Channel/Client factory do something like this:

 private function createChannel(string $host, array $args = []): \Grpc\Channel
    {
        $channel = new \Grpc\Channel($host, $args);
        return \Grpc\Interceptor::intercept($channel, $this->interceptors);
    }

diutsu avatar Jul 13 '21 12:07 diutsu

@diutsu your method worked fantastically, thank you!

One minor change was where we set the service tag: $scope->getSpan()->setTag(Tag::SERVICE_NAME, getenv('DD_SERVICE')); - this way we can use the interceptor in multiple services.

For anyone else unsure about how to use the diutsu's method, you pass the channel that you create to the constructor of your php grpc service class e.g. new FooServiceClient('foo-service:5000', ['credentials' => null], $channel);.

alexkb avatar Aug 23 '23 02:08 alexkb