diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ac750e68..405df66d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -179,6 +179,7 @@ jobs: gcc \ libc-dev \ libpqxx-dev \ + zlib1g-dev \ make \ autoconf \ git \ @@ -191,6 +192,11 @@ jobs: ./configure --enable-opencensus sudo make install sudo docker-php-ext-enable opencensus + - run: + name: Install grpc extension + command: | + sudo pecl install grpc + sudo docker-php-ext-enable grpc - run: name: Install memcached extension command: | @@ -210,6 +216,9 @@ jobs: - run: name: Curl test command: tests/integration/curl/test.sh + - run: + name: Grpc test + command: tests/integration/grpc/test.sh - run: name: Guzzle 5 test command: tests/integration/guzzle5/test.sh diff --git a/src/Trace/Integrations/Grpc.php b/src/Trace/Integrations/Grpc.php index c49214a7d..d50e0ed9a 100644 --- a/src/Trace/Integrations/Grpc.php +++ b/src/Trace/Integrations/Grpc.php @@ -18,6 +18,7 @@ namespace OpenCensus\Trace\Integrations; use Grpc\BaseStub; +use OpenCensus\Trace\Integrations\Grpc\TraceInterceptor; use OpenCensus\Trace\Span; use OpenCensus\Trace\Tracer; use OpenCensus\Trace\Propagator\GrpcMetadataPropagator; @@ -39,6 +40,8 @@ class Grpc implements IntegrationInterface */ public static function load() { + trigger_error(static::class . ' is deprecated. Please use the ' . TraceInterceptor::class, E_USER_DEPRECATED); + if (!extension_loaded('opencensus')) { trigger_error('opencensus extension required to load grpc integrations.', E_USER_WARNING); return; diff --git a/src/Trace/Integrations/Grpc/TraceInterceptor.php b/src/Trace/Integrations/Grpc/TraceInterceptor.php new file mode 100644 index 000000000..ee239cae1 --- /dev/null +++ b/src/Trace/Integrations/Grpc/TraceInterceptor.php @@ -0,0 +1,163 @@ +propagator = $propagator ?: new GrpcMetadataPropagator(); + } + + /** + * This interceptor is for simple unary requests. + * + * @param string $method The name of the method to call + * @param mixed $argument The argument to the method + * @param array $metadata A metadata map to send to the server (optional) + * @param array $options An array of options (optional) + * @param callable $continuation The next interceptor to call + */ + public function interceptUnaryUnary( + $method, + $argument, + array $metadata, + array $options, + $continuation + ) { + $spanOptions = [ + 'name' => 'grpc/simpleRequest', + 'attributes' => [ + 'method' => $method + ], + 'kind' => Span::KIND_CLIENT + ]; + $metadata = $this->injectMetadata($metadata); + + return Tracer::inSpan($spanOptions, $continuation, [$method, $argument, $metadata, $options]); + } + + /** + * This interceptor is for client streaming requests. + * + * @param string $method The name of the method to call + * @param array $metadata A metadata map to send to the server (optional) + * @param array $options An array of options (optional) + * @param callable $continuation The next interceptor to call + */ + public function interceptStreamUnary( + $method, + array $metadata, + array $options, + $continuation + ) { + $spanOptions = [ + 'name' => 'grpc/clientStreamRequest', + 'attributes' => [ + 'method' => $method + ], + 'kind' => Span::KIND_CLIENT + ]; + $metadata = $this->injectMetadata($metadata); + return Tracer::inSpan($spanOptions, $continuation, [$method, $metadata, $options]); + } + + /** + * This interceptor is for server streaming requests. + * + * @param string $method The name of the method to call + * @param mixed $argument The argument to the method + * @param array $metadata A metadata map to send to the server (optional) + * @param array $options An array of options (optional) + * @param callable $continuation The next interceptor to call + */ + public function interceptUnaryStream( + $method, + $argument, + array $metadata, + array $options, + $continuation + ) { + $spanOptions = [ + 'name' => 'grpc/serverStreamRequest', + 'attributes' => [ + 'method' => $method + ], + 'kind' => Span::KIND_CLIENT + ]; + $metadata = $this->injectMetadata($metadata); + return Tracer::inSpan($spanOptions, $continuation, [$method, $argument, $metadata, $options]); + } + + /** + * This interceptor is for server streaming requests. + * + * @param string $method The name of the method to call + * @param array $metadata A metadata map to send to the server (optional) + * @param array $options An array of options (optional) + * @param callable $continuation The next interceptor to call + */ + public function interceptStreamStream( + $method, + array $metadata, + array $options, + $continuation + ) { + $spanOptions = [ + 'name' => 'grpc/bidiRequest', + 'attributes' => [ + 'method' => $method + ], + 'kind' => Span::KIND_CLIENT + ]; + $metadata = $this->injectMetadata($metadata); + return Tracer::inSpan($spanOptions, $continuation, [$method, $metadata, $options]); + } + + private function injectMetadata(array $metadata) + { + $context = Tracer::spanContext(); + return $this->propagator->inject($context, $metadata); + } +} diff --git a/src/Trace/Propagator/GrpcMetadataPropagator.php b/src/Trace/Propagator/GrpcMetadataPropagator.php index f897f9cb9..a3e668af5 100644 --- a/src/Trace/Propagator/GrpcMetadataPropagator.php +++ b/src/Trace/Propagator/GrpcMetadataPropagator.php @@ -75,7 +75,7 @@ public function extract($metadata) */ public function inject(SpanContext $context, $metadata) { - $metadata[$this->key] = $this->formatter->serialize($context); + $metadata[$this->key] = [$this->formatter->serialize($context)]; return $metadata; } diff --git a/tests/integration/grpc/composer.json b/tests/integration/grpc/composer.json new file mode 100644 index 000000000..f41ee414a --- /dev/null +++ b/tests/integration/grpc/composer.json @@ -0,0 +1,22 @@ +{ + "require": { + "php": "^7.2", + "opencensus/opencensus": "dev-master", + "grpc/grpc": "dev-master", + "ext-opencensus": "*", + "ext-grpc": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "repositories": { + "opencensus": { + "type": "git", + "url": "https://github.com/census-instrumentation/opencensus-php" + }, + "grpc": { + "type": "git", + "url": "https://github.com/grpc/grpc" + } + } +} \ No newline at end of file diff --git a/tests/integration/grpc/phpunit.xml.dist b/tests/integration/grpc/phpunit.xml.dist new file mode 100644 index 000000000..0ba38fc79 --- /dev/null +++ b/tests/integration/grpc/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + tests + + + + + src + + + diff --git a/tests/integration/grpc/test.sh b/tests/integration/grpc/test.sh new file mode 100755 index 000000000..f7ca3324a --- /dev/null +++ b/tests/integration/grpc/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright 2018 OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +pushd $(dirname ${BASH_SOURCE[0]}) +source ../setup_test_repo.sh + +sed -i "s|\"opencensus/opencensus\": \"dev-master\"|\"opencensus/opencensus\": \"dev-${BRANCH}\"|" composer.json +sed -i "s|https://github.com/census-instrumentation/opencensus-php|${REPO}|" composer.json +composer install -n --prefer-dist + +vendor/bin/phpunit + +popd diff --git a/tests/integration/grpc/tests/GrpcTest.php b/tests/integration/grpc/tests/GrpcTest.php new file mode 100644 index 000000000..d4b9ba75a --- /dev/null +++ b/tests/integration/grpc/tests/GrpcTest.php @@ -0,0 +1,185 @@ +server = new Server([]); + $this->port = $this->server->addHttp2Port('0.0.0.0:0'); + $this->channel = new Channel( + 'localhost:' . $this->port, [ + 'credentials' => ChannelCredentials::createInsecure() + ] + ); + $this->server->start(); + + if (extension_loaded('opencensus')) { + opencensus_trace_clear(); + } + } + + public function tearDown() + { + $this->channel->close(); + parent::tearDown(); + } + + public function testGrpcUnaryUnary() + { + $exporter = $this->prophesize(ExporterInterface::class); + $tracer = Tracer::start($exporter->reveal(), [ + 'skipReporting' => true + ]); + + $interceptor = new TraceInterceptor(); + $channel = Interceptor::intercept($this->channel, $interceptor); + $client = new InterceptorClient('localhost' . $this->port, [ + 'credentials' => ChannelCredentials::createInsecure() + ], $channel); + + $request = new SimpleRequest('grpc_unary_data'); + + $call = $client->UnaryCall($request); + $event = $this->server->requestCall(); + $this->assertEquals('/dummy_method', $event->method); + $this->assertArrayHasKey('grpc-trace-bin', $event->metadata); + + $spans = $tracer->tracer()->spans(); + $this->assertCount(2, $spans); + + $unarySpan = $spans[1]; + $this->assertEquals('grpc/simpleRequest', $unarySpan->name()); + $this->assertEquals(Span::KIND_CLIENT, $unarySpan->kind()); + } + + public function testGrpcStreamUnary() + { + $exporter = $this->prophesize(ExporterInterface::class); + $tracer = Tracer::start($exporter->reveal(), [ + 'skipReporting' => true + ]); + + $interceptor = new TraceInterceptor(); + $channel = Interceptor::intercept($this->channel, $interceptor); + $client = new InterceptorClient('localhost' . $this->port, [ + 'credentials' => ChannelCredentials::createInsecure() + ], $channel); + + $request = new SimpleRequest('grpc_unary_data'); + + $streamCall = $client->StreamCall(); + $streamCall->write($request); + $event = $this->server->requestCall(); + $this->assertEquals('/dummy_method', $event->method); + $this->assertArrayHasKey('grpc-trace-bin', $event->metadata); + + $spans = $tracer->tracer()->spans(); + $this->assertCount(2, $spans); + + $streamSpam = $spans[1]; + $this->assertEquals('grpc/clientStreamRequest', $streamSpam->name()); + $this->assertEquals(Span::KIND_CLIENT, $streamSpam->kind()); + + } +} + +class InterceptorClient extends BaseStub +{ + /** + * @param string $hostname hostname + * @param array $opts channel options + * @param Channel|InterceptorChannel $channel (optional) re-use channel object + */ + public function __construct($hostname, $opts, $channel = null) + { + parent::__construct($hostname, $opts, $channel); + } + /** + * A simple RPC. + * @param \Routeguide\Point $argument input argument + * @param array $metadata metadata + * @param array $options call options + */ + public function UnaryCall( + SimpleRequest $argument, + $metadata = [], + $options = [] + ) { + return $this->_simpleRequest( + '/dummy_method', + $argument, + [], + $metadata, + $options + ); + } + /** + * A client-to-server streaming RPC. + * @param array $metadata metadata + * @param array $options call options + */ + public function StreamCall( + $metadata = [], + $options = [] + ) { + return $this->_clientStreamRequest('/dummy_method', [], $metadata, $options); + } +} + +class SimpleRequest +{ + private $data; + public function __construct($data) + { + $this->data = $data; + } + public function setData($data) + { + $this->data = $data; + } + public function serializeToString() + { + return $this->data; + } +} \ No newline at end of file