diff --git a/README.md b/README.md index 5e98885ac..0910c9054 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ The provided exporters are: | [LoggerExporter][logger-exporter] | Exporter JSON encoded spans to a PSR-3 logger | | | [NullExporter][null-exporter] | No-op | | | [OneLineEchoExporter][one-line-echo-exporter] | Output the collected spans to stdout with one-line | | -| [StackdriverExporter][stackdriver-exporter] | Report traces to Google Cloud Stackdriver Trace | | -| [ZipkinExporter][zipkin-exporter] | Report collected spans to a Zipkin server | | +| [StackdriverExporter][stackdriver-exporter] | Report traces to Google Cloud Stackdriver Trace | [opencensus/opencensus-exporter-stackdriver][stackdriver-packagist] | +| [ZipkinExporter][zipkin-exporter] | Report collected spans to a Zipkin server | [opencensus/opencensus-exporter-zipkin][zipkin-packagist] | If you would like to provide your own reporter, create a class that implements `ExporterInterface`. @@ -168,10 +168,12 @@ This is not an official Google product. [echo-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/EchoExporter.html [one-line-echo-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/OneLineEchoExporter.html [file-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/FileExporter.html -[jaeger-exporter]: https://github.com/census-instrumentation/opencensus-php-exporter-jaeger/blob/master/src/JaegerExporter.php +[jaeger-exporter]: https://github.com/census-ecosystem/opencensus-php-exporter-jaeger [jaeger-packagist]: https://packagist.org/packages/opencensus/opencensus-exporter-jaeger [logger-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/LoggerExporter.html [null-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/NullExporter.html -[stackdriver-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/StackdriverExporter.html -[zipkin-exporter]: https://opencensus.io/api/php/api/master/OpenCensus/Trace/Exporter/ZipkinExporter.html +[stackdriver-exporter]: https://github.com/census-ecosystem/opencensus-php-exporter-stackdriver +[stackdriver-packagist]: https://packagist.org/packages/opencensus/opencensus-exporter-stackdriver +[zipkin-exporter]: https://github.com/census-ecosystem/opencensus-php-exporter-zipkin +[zipkin-packagist]: https://packagist.org/packages/opencensus/opencensus-exporter-zipkin [semver]: http://semver.org/ diff --git a/composer.json b/composer.json index 5a2134c9c..9f927a385 100644 --- a/composer.json +++ b/composer.json @@ -20,15 +20,18 @@ "require-dev": { "phpunit/phpunit": "^5.0", "squizlabs/php_codesniffer": "2.*", - "google/cloud-trace": "^0.4", "twig/twig": "~2.0 || ~1.35", "symfony/yaml": "~3.3", - "guzzlehttp/guzzle": "~5.3" + "guzzlehttp/guzzle": "~5.3", + "guzzlehttp/psr7": "~1.4" }, "conflict": { "ext-opencensus": "< 0.1.0" }, "suggest": { + "opencensus/opencensus-exporter-jaeger": "Export data to Jaeger", + "opencensus/opencensus-exporter-stackdriver": "Export data to Stackdriver", + "opencensus/opencensus-exporter-zipkin": "Export data to Zipkin", "ext-opencensus": "Enable tracing arbitrary functions.", "cache/apcu-adapter": "Enable QpsSampler to use apcu cache.", "cache/apc-adapter": "Enable QpsSampler to use apc cache.", diff --git a/src/Trace/Exporter/StackdriverExporter.php b/src/Trace/Exporter/StackdriverExporter.php deleted file mode 100644 index 5262ea4b7..000000000 --- a/src/Trace/Exporter/StackdriverExporter.php +++ /dev/null @@ -1,231 +0,0 @@ - [ - * 'projectId' => 'my-project' - * ] - * ]); - * Tracer::start($reporter); - * ``` - * - * The above configuration will synchronously report the traces to Google Cloud - * Stackdriver Trace. You can enable an experimental asynchronous reporting - * mechanism using - * BatchDaemon. - * - * Example: - * ``` - * use OpenCensus\Trace\Tracer; - * use OpenCensus\Trace\Exporter\StackdriverExporter; - * - * $reporter = new StackdriverExporter([ - * 'async' => true, - * 'clientConfig' => [ - * 'projectId' => 'my-project' - * ] - * ]); - * Tracer::start($reporter); - * ``` - * - * Note that to use the `async` option, you will also need to set the - * `IS_BATCH_DAEMON_RUNNING` environment variable to `true`. - * - * @experimental The experimental flag means that while we believe this method - * or class is ready for use, it may change before release in backwards- - * incompatible ways. Please use with caution, and test thoroughly when - * upgrading. - */ -class StackdriverExporter implements ExporterInterface -{ - const ATTRIBUTE_MAP = [ - OCSpan::ATTRIBUTE_HOST => '/http/host', - OCSpan::ATTRIBUTE_PORT => '/http/port', - OCSpan::ATTRIBUTE_METHOD => '/http/method', - OCSpan::ATTRIBUTE_PATH => '/http/url', - OCSpan::ATTRIBUTE_USER_AGENT => '/http/user_agent', - OCSpan::ATTRIBUTE_STATUS_CODE => '/http/status_code' - ]; - const AGENT = 'g.co/agent'; - - use BatchTrait; - - /** - * @var TraceClient - */ - private static $client; - - /** - * @var bool - */ - private $async; - - /** - * Create a TraceExporter that utilizes background batching. - * - * @param array $options [optional] Configuration options. - * - * @type TraceClient $client A trace client used to instantiate traces - * to be delivered to the batch queue. - * @type bool $debugOutput Whether or not to output debug information. - * Please note debug output currently only applies in CLI based - * applications. **Defaults to** `false`. - * @type array $batchOptions A set of options for a BatchJob. See - * \Google\Cloud\Core\Batch\BatchJob::__construct() - * for more details. - * **Defaults to** ['batchSize' => 1000, - * 'callPeriod' => 2.0, - * 'workerNum' => 2]. - * @type array $clientConfig Configuration options for the Trace client - * used to handle processing of batch items. - * For valid options please see - * \Google\Cloud\Trace\TraceClient::__construct(). - * @type BatchRunner $batchRunner A BatchRunner object. Mainly used for - * the tests to inject a mock. **Defaults to** a newly created - * BatchRunner. - * @type string $identifier An identifier for the batch job. - * **Defaults to** `stackdriver-trace`. - * @type bool $async Whether we should try to use the batch runner. - * **Defaults to** `false`. - */ - public function __construct(array $options = []) - { - $this->async = isset($options['async']) ? $options['async'] : false; - $this->setCommonBatchProperties($options + [ - 'identifier' => 'stackdriver-trace', - 'batchMethod' => 'insertBatch' - ]); - self::$client = isset($options['client']) - ? $options['client'] - : new TraceClient($this->clientConfig); - } - - /** - * Report the provided Trace to a backend. - * - * @param SpanData[] $spans - * @return bool - */ - public function export(array $spans) - { - $spans = $this->convertSpans($spans); - - if (empty($spans)) { - return false; - } - - // Pull the traceId from the first span - $rootSpan = $spans[0]; - $trace = self::$client->trace( - $rootSpan->traceId() - ); - $rootSpan->addAttribute( - self::AGENT, - sprintf('opencensus-php [%s]', Version::VERSION) - ); - - // build a Trace object and assign Spans - $trace->setSpans($spans); - - try { - if ($this->async) { - return $this->batchRunner->submitItem($this->identifier, $trace); - } else { - return self::$client->insert($trace); - } - } catch (\Exception $e) { - error_log('Reporting the Trace data failed: ' . $e->getMessage()); - return false; - } - } - - /** - * Convert spans into Stackdriver's expected JSON output format. - * - * @access private - * - * @param SpanData[] $spans - * @return Span[] Representation of the collected trace spans ready for - * serialization - */ - public function convertSpans(array $spans) - { - // transform OpenCensus Spans to Google\Cloud\Trace\Spans - return array_map([$this, 'mapSpan'], $spans); - } - - private function mapSpan(SpanData $span) - { - return new Span($span->traceId(), [ - 'name' => $span->name(), - 'startTime' => $span->startTime(), - 'endTime' => $span->endTime(), - 'spanId' => $span->spanId(), - 'parentSpanId' => $span->parentSpanId(), - 'attributes' => $this->mapAttributes($span->attributes()), - 'stackTrace' => $span->stackTrace() - ]); - } - - private function mapAttributes(array $attributes) - { - $newAttributes = []; - foreach ($attributes as $key => $value) { - if (array_key_exists($key, self::ATTRIBUTE_MAP)) { - $newAttributes[self::ATTRIBUTE_MAP[$key]] = $value; - } else { - $newAttributes[$key] = $value; - } - } - return $newAttributes; - } - - /** - * Returns an array representation of a callback which will be used to write - * batch items. - * - * @return array - */ - protected function getCallback() - { - if (!isset(self::$client)) { - self::$client = new TraceClient($this->clientConfig); - } - - return [self::$client, $this->batchMethod]; - } -} diff --git a/src/Trace/Exporter/ZipkinExporter.php b/src/Trace/Exporter/ZipkinExporter.php deleted file mode 100644 index 13ed143ba..000000000 --- a/src/Trace/Exporter/ZipkinExporter.php +++ /dev/null @@ -1,225 +0,0 @@ - null, - Span::KIND_SERVER => self::KIND_SERVER, - Span::KIND_CLIENT => self::KIND_CLIENT - ]; - - /** - * @var string - */ - private $endpointUrl; - - /** - * @var array - */ - private $localEndpoint; - - /** - * Create a new ZipkinExporter - * - * @param string $name The name of this application - * @param string $endpointUrl (optional) The url for the span reporting - * endpoint. **Defaults to** `http://localhost:9411/api/v2/spans` - * @param array $server (optional) The server array to search for the - * SERVER_PORT. **Defaults to** $_SERVER - */ - public function __construct($name, $endpointUrl = null, array $server = null) - { - $server = $server ?: $_SERVER; - $this->endpointUrl = ($endpointUrl === null) ? self::DEFAULT_ENDPOINT : $endpointUrl; - $this->localEndpoint = [ - 'serviceName' => $name - ]; - if (array_key_exists('SERVER_PORT', $server)) { - $this->localEndpoint['port'] = intval($server['SERVER_PORT']); - } - } - - /** - * Set the localEndpoint ipv4 value for all reported spans. Note that this - * is optional because the reverse DNS lookup can be slow. - * - * @param string $ipv4 IPv4 address - */ - public function setLocalIpv4($ipv4) - { - $this->localEndpoint['ipv4'] = $ipv4; - } - - /** - * Set the localEndpoint ipv6 value for all reported spans. Note that this - * is optional because the reverse DNS lookup can be slow. - * - * @param string $ipv6 IPv6 address - */ - public function setLocalIpv6($ipv6) - { - $this->localEndpoint['ipv6'] = $ipv6; - } - - /** - * Report the provided Trace to a backend. - * - * @param SpanData[] $spans - * @return bool - */ - public function export(array $spans) - { - $spans = $this->convertSpans($spans); - - if (empty($spans)) { - return false; - } - - try { - $json = json_encode($spans); - $contextOptions = [ - 'http' => [ - 'method' => 'POST', - 'header' => 'Content-Type: application/json', - 'content' => $json - ] - ]; - - $context = stream_context_create($contextOptions); - file_get_contents($this->endpointUrl, false, $context); - } catch (\Exception $e) { - return false; - } - return true; - } - - /** - * Convert spans into Zipkin's expected JSON output format. See - * output format definition. - * - * @param SpanData[] $spans - * @param array $headers [optional] HTTP headers to parse. **Defaults to** $_SERVER - * @return array Representation of the collected trace spans ready for serialization - */ - public function convertSpans(array $spans, $headers = null) - { - $headers = $headers ?: $_SERVER; - - // True is a request to store this span even if it overrides sampling policy. - // This is true when the X-B3-Flags header has a value of 1. - $isDebug = array_key_exists('HTTP_X_B3_FLAGS', $headers) && $headers['HTTP_X_B3_FLAGS'] == '1'; - - // True if we are contributing to a span started by another tracer (ex on a different host). - $isShared = !empty($spans) && $spans[0]->parentSpanId() !== null; - - $zipkinSpans = []; - foreach ($spans as $span) { - $startTime = (int)((float) $span->startTime()->format('U.u') * 1000 * 1000); - $endTime = (int)((float) $span->endTime()->format('U.u') * 1000 * 1000); - $spanId = str_pad($span->spanId(), 16, '0', STR_PAD_LEFT); - $parentSpanId = $span->parentSpanId() - ? str_pad($span->parentSpanId(), 16, '0', STR_PAD_LEFT) - : null; - $traceId = str_pad($span->traceId(), 32, '0', STR_PAD_LEFT); - - $attributes = $span->attributes(); - if (empty($attributes)) { - // force json_encode to render an empty object ("{}") instead of an empty array ("[]") - $attributes = new \stdClass(); - } - - $zipkinSpan = [ - 'traceId' => $traceId, - 'name' => $span->name(), - 'parentId' => $parentSpanId, - 'id' => $spanId, - 'timestamp' => $startTime, - 'duration' => $endTime - $startTime, - 'debug' => $isDebug, - 'shared' => $isShared, - 'localEndpoint' => $this->localEndpoint, - 'tags' => $attributes, - ]; - - if (null !== ($kind = $this->spanKind($span))) { - $zipkinSpan['kind'] = $kind; - } - - $zipkinSpans[] = $zipkinSpan; - } - - return $zipkinSpans; - } - - private function spanKind(SpanData $span) - { - $kind = self::KIND_MAP[$span->kind()]; - if ($kind !== null) { - return $kind; - } - - if (strpos($span->name(), 'Sent.') === 0) { - return self::KIND_CLIENT; - } - - if (strpos($span->name(), 'Recv.') === 0) { - return self::KIND_SERVER; - } - - if ($span->timeEvents()) { - foreach ($span->timeEvents() as $event) { - if (!($event instanceof MessageEvent)) { - continue; - } - - switch ($event->type()) { - case MessageEvent::TYPE_SENT: - return self::KIND_CLIENT; - break; - case MessageEvent::TYPE_RECEIVED: - return self::KIND_SERVER; - break; - } - } - } - - return null; - } -} diff --git a/tests/unit/Trace/Exporter/StackdriverExporterTest.php b/tests/unit/Trace/Exporter/StackdriverExporterTest.php deleted file mode 100644 index f835d1f2f..000000000 --- a/tests/unit/Trace/Exporter/StackdriverExporterTest.php +++ /dev/null @@ -1,190 +0,0 @@ -client = $this->prophesize(TraceClient::class); - - $this->spans = array_map(function ($span) { - return $span->spanData(); - }, [ - new OCSpan([ - 'name' => 'span', - 'startTime' => microtime(true), - 'endTime' => microtime(true) + 10 - ]) - ]); - } - - public function testFormatsTrace() - { - $exporter = new StackdriverExporter(['client' => $this->client->reveal()]); - $spans = $exporter->convertSpans($this->spans); - - foreach ($spans as $span) { - $this->assertInstanceOf(Span::class, $span); - $this->assertInternalType('string', $span->name()); - $this->assertInternalType('string', $span->spanId()); - $this->assertRegExp('/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z/', $span->jsonSerialize()['startTime']); - $this->assertRegExp('/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z/', $span->jsonSerialize()['endTime']); - } - } - - public function testReportWithAnExceptionErrorLog() - { - $this->client->insert(Argument::any())->willThrow( - new \Exception('error_log test') - ); - $trace = $this->prophesize(Trace::class); - $trace->setSpans(Argument::any())->shouldBeCalled(); - $this->client->trace(Argument::any())->willReturn($trace->reveal()); - $exporter = new StackdriverExporter( - ['client' => $this->client->reveal()] - ); - $this->expectOutputString( - 'Reporting the Trace data failed: error_log test' - ); - $this->assertFalse($exporter->export($this->spans)); - } - - public function testStacktrace() - { - $stackTrace = [ - [ - 'file' => '/path/to/file.php', - 'class' => 'Foo', - 'line' => 1234, - 'function' => 'asdf', - 'type' => '::' - ] - ]; - $span = new OCSpan([ - 'stackTrace' => $stackTrace - ]); - $span->setStartTime(); - $span->setEndTime(); - - $exporter = new StackdriverExporter(['client' => $this->client->reveal()]); - $spans = $exporter->convertSpans([$span->spanData()]); - - $data = $spans[0]->jsonSerialize(); - $this->assertArrayHasKey('stackTrace', $data); - } - - public function testEmptyTrace() - { - $exporter = new StackdriverExporter(['client' => $this->client->reveal()]); - $this->assertFalse($exporter->export([])); - } - - /** - * @dataProvider attributesToTest - */ - public function testMapsAttributes($key, $value, $expectedAttributeKey, $expectedAttributeValue) - { - $tracer = new ContextTracer(new SpanContext('testtraceid')); - $tracer->inSpan([ - 'name' => 'span', - 'attributes' => [ - $key => $value - ] - ], function () {}); - - $span = new OCSpan([ - 'attributes' => [ - $key => $value - ] - ]); - $span->setStartTime(); - $span->setEndTime(); - - $exporter = new StackdriverExporter(['client' => $this->client->reveal()]); - $spans = $exporter->convertSpans([$span->spanData()]); - $this->assertCount(1, $spans); - $span = $spans[0]; - - $attributes = $span->jsonSerialize()['attributes']; - $this->assertArrayHasKey($expectedAttributeKey, $attributes); - $this->assertEquals($expectedAttributeValue, $attributes[$expectedAttributeKey]); - } - - public function attributesToTest() - { - return [ - ['http.host', 'foo.example.com', '/http/host', 'foo.example.com'], - ['http.port', '80', '/http/port', '80'], - ['http.path', '/foobar', '/http/url', '/foobar'], - ['http.method', 'PUT', '/http/method', 'PUT'], - ['http.user_agent', 'user agent', '/http/user_agent', 'user agent'] - ]; - } - - public function testReportsVersionAttribute() - { - $trace = $this->prophesize(Trace::class); - $trace->setSpans(Argument::that(function ($spans) { - $this->assertCount(1, $spans); - $attributes = $spans[0]->jsonSerialize()['attributes']; - $this->assertArrayHasKey('g.co/agent', $attributes); - $this->assertRegexp('/\d+\.\d+\.\d+/', $attributes['g.co/agent']); - return true; - }))->shouldBeCalled(); - $this->client->trace('aaa')->willReturn($trace->reveal()); - $this->client->insert(Argument::type(Trace::class)) - ->willReturn(true)->shouldBeCalled(); - - $span = new OCSpan([ - 'traceId' => 'aaa' - ]); - $span->setStartTime(); - $span->setEndTime(); - - $exporter = new StackdriverExporter(['client' => $this->client->reveal()]); - $this->assertTrue($exporter->export([$span->spanData()])); - } -} diff --git a/tests/unit/Trace/Exporter/ZipkinExporterTest.php b/tests/unit/Trace/Exporter/ZipkinExporterTest.php deleted file mode 100644 index 8d095f8ed..000000000 --- a/tests/unit/Trace/Exporter/ZipkinExporterTest.php +++ /dev/null @@ -1,191 +0,0 @@ -spans = array_map(function ($span) { - return $span->spanData(); - }, [ - new Span([ - 'traceId' => 'aaa', - 'name' => 'span', - 'startTime' => microtime(true), - 'endTime' => microtime(true) + 10 - ]) - ]); - } - - /** - * http://zipkin.io/zipkin-api/#/paths/%252Fspans/post - */ - public function testFormatsTrace() - { - $exporter = new ZipkinExporter('myapp'); - $data = $exporter->convertSpans($this->spans); - - $this->assertInternalType('array', $data); - foreach ($data as $span) { - $this->assertRegExp('/[0-9a-z]{16}/', $span['id']); - $this->assertRegExp('/[0-9a-z]{32}/', $span['traceId']); - $this->assertInternalType('string', $span['name']); - $this->assertInternalType('int', $span['timestamp']); - $this->assertInternalType('int', $span['duration']); - - // make sure we have a JSON object, even when there is no tags - $this->assertStringStartsWith('{', \json_encode($span['tags'])); - $this->assertStringEndsWith('}', \json_encode($span['tags'])); - - foreach ($span['tags'] as $key => $value) { - $this->assertInternalType('string', $key); - $this->assertInternalType('string', $value); - } - $this->assertFalse($span['shared']); - $this->assertFalse($span['debug']); - } - } - - /** - * @dataProvider spanOptionsForKind - */ - public function testSpanKind($spanOpts, $kind) - { - $span = new Span($spanOpts); - $span->setStartTime(); - $span->setEndTime(); - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans([$span->spanData()]); - - $this->assertEquals($kind, $spans[0]['kind']); - } - - public function testUnspecifiedSpanKind() - { - $span = new Span([ - 'kind' => Span::KIND_UNSPECIFIED - ]); - $span->setStartTime(); - $span->setEndTime(); - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans([$span->spanData()]); - - $this->assertArrayNotHasKey('kind', $spans[0]); - } - - public function spanOptionsForKind() - { - return [ - [['name' => 'Recv.Span1'], 'SERVER'], - [['name' => 'Sent.Span2'], 'CLIENT'], - [['name' => 'span3', 'timeEvents' => [new MessageEvent(MessageEvent::TYPE_RECEIVED, '')]], 'SERVER'], - [['name' => 'span4', 'timeEvents' => [new MessageEvent(MessageEvent::TYPE_SENT, '')]], 'CLIENT'], - [['kind' => Span::KIND_SERVER], 'SERVER'], - [['kind' => Span::KIND_CLIENT], 'CLIENT'] - ]; - } - - public function testSpanDebug() - { - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans($this->spans, [ - 'HTTP_X_B3_FLAGS' => '1' - ]); - - $this->assertCount(1, $spans); - $this->assertTrue($spans[0]['debug']); - } - - public function testSpanShared() - { - $span = new Span(['parentSpanId' => 'abc']); - $span->setStartTime(); - $span->setEndTime(); - - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans([$span->spanData()]); - - $this->assertCount(1, $spans); - $this->assertTrue($spans[0]['shared']); - } - - public function testEmptyTrace() - { - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans([]); - $this->assertEmpty($spans); - } - - public function testSkipsIpv4() - { - $exporter = new ZipkinExporter('myapp'); - $spans = $exporter->convertSpans($this->spans); - - $endpoint = $spans[0]['localEndpoint']; - $this->assertArrayNotHasKey('ipv4', $endpoint); - $this->assertArrayNotHasKey('ipv6', $endpoint); - } - - public function testSetsIpv4() - { - $exporter = new ZipkinExporter('myapp'); - $exporter->setLocalIpv4('1.2.3.4'); - $spans = $exporter->convertSpans($this->spans); - - $endpoint = $spans[0]['localEndpoint']; - $this->assertArrayHasKey('ipv4', $endpoint); - $this->assertEquals('1.2.3.4', $endpoint['ipv4']); - } - - public function testSetsIpv6() - { - $exporter = new ZipkinExporter('myapp'); - $exporter->setLocalIpv6('2001:db8:85a3::8a2e:370:7334'); - $spans = $exporter->convertSpans($this->spans); - - $endpoint = $spans[0]['localEndpoint']; - $this->assertArrayHasKey('ipv6', $endpoint); - $this->assertEquals('2001:db8:85a3::8a2e:370:7334', $endpoint['ipv6']); - } - - public function testSetsLocalEndpointPort() - { - $exporter = new ZipkinExporter('myapp', null, ['SERVER_PORT' => "80"]); - $spans = $exporter->convertSpans($this->spans); - - $endpoint = $spans[0]['localEndpoint']; - $this->assertArrayHasKey('port', $endpoint); - $this->assertEquals(80, $endpoint['port']); - } -} diff --git a/tests/unit/Trace/Exporter/mock_error_log.php b/tests/unit/Trace/Exporter/mock_error_log.php index f33787d84..58f26037c 100644 --- a/tests/unit/Trace/Exporter/mock_error_log.php +++ b/tests/unit/Trace/Exporter/mock_error_log.php @@ -24,4 +24,4 @@ function error_log($msg) { echo $msg; -} +} \ No newline at end of file