diff --git a/composer.json b/composer.json index 2fb4c0793..c35025ae3 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "php-http/async-client-implementation": "^1.0", "php-http/discovery": "^1.14", "promphp/prometheus_client_php": "^2.2.1", + "psr/event-dispatcher": "^1", "psr/http-factory-implementation": "^1.0", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-mbstring": "^1.23" diff --git a/deptrac.yaml b/deptrac.yaml index 1bb78f1b7..fc325477b 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -52,6 +52,10 @@ deptrac: collectors: - type: className regex: ^Grpc\\* + - name: PsrEventDispatcher + collectors: + - type: className + regex: ^Psr\\EventDispatcher\\ - name: PsrLog collectors: - type: className @@ -99,6 +103,8 @@ deptrac: SemConv: ~ API: - Context + - PsrEventDispatcher + - PsrLog - SemConv SDK: - +API diff --git a/examples/EventsExample.php b/examples/EventsExample.php new file mode 100644 index 000000000..afb9dc6f8 --- /dev/null +++ b/examples/EventsExample.php @@ -0,0 +1,68 @@ +setFormatter(new JsonFormatter())]) +); + +/** + * Register event listeners - this only works for SimpleEventDispatcher. For other PSR-14 implementations, you will +/* need to register all events that you are interested in, @see {SDK\Event\EventTypes} + */ +$listenerProvider = new SimpleListenerProvider(); +$listenerProvider->listen(EventType::ERROR, function (ErrorEvent $event) { + echo 'Custom handling of an error event: ' . $event->getException()->getMessage() . PHP_EOL; +}, -10); //runs before built-in handler +$listenerProvider->listen(EventType::ERROR, function (ErrorEvent $event) { + echo 'Another custom handling of an error event: ' . $event->getMessage() . PHP_EOL; + echo 'Trace: ' . json_encode($event->getException()->getTrace()) . PHP_EOL; + echo 'Stopping event propagation...' . PHP_EOL; + $event->stopPropagation(); +}, 5); //runs after built-in handler +$listenerProvider->listen(EventType::ERROR, function (ErrorEvent $event) { + echo 'This will not be executed, because a higher priority handler stopped event propagation.' . PHP_EOL; +}, 10); + +Dispatcher::setInstance(new SimpleDispatcher($listenerProvider)); + +//or, provide your own PSR-14 event dispatcher. This must be done before getting a tracer: +//$listenerProvider = new \Any\Psr14\ListenerProvider(); +//$listenerProvider->listen(EventType::ERROR, function(ErrorEvent $event){...}); +//DispatcherHolder::setInstance(\Any\Psr14\EventDispatcherInterface($listenerProvider)); + +$tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + ZipkinExporter::fromConnectionString('http://invalid-host:9999', 'zipkin-exporter') + ) +); +$tracer = $tracerProvider->getTracer(); + +$span = $tracer->spanBuilder('root')->startSpan(); +$span->end(); diff --git a/examples/SettingUpLogging.php b/examples/SettingUpLogging.php index 2c7e3d984..0046a2975 100644 --- a/examples/SettingUpLogging.php +++ b/examples/SettingUpLogging.php @@ -5,8 +5,8 @@ use Monolog\Handler\StreamHandler; use Monolog\Logger; +use OpenTelemetry\API\Common\Log\LoggerHolder; use OpenTelemetry\Contrib\OtlpGrpc\Exporter as OtlpGrpcExporter; -use OpenTelemetry\SDK\Common\Log\LoggerHolder; use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor; use OpenTelemetry\SDK\Trace\TracerProvider; use Psr\Log\LogLevel; diff --git a/src/API/Behavior/EmitsEventsTrait.php b/src/API/Behavior/EmitsEventsTrait.php new file mode 100644 index 000000000..36be9f012 --- /dev/null +++ b/src/API/Behavior/EmitsEventsTrait.php @@ -0,0 +1,15 @@ +dispatch($event); + } +} diff --git a/src/SDK/Behavior/LogsMessagesTrait.php b/src/API/Behavior/LogsMessagesTrait.php similarity index 88% rename from src/SDK/Behavior/LogsMessagesTrait.php rename to src/API/Behavior/LogsMessagesTrait.php index 23b3fb7d2..244cf5201 100644 --- a/src/SDK/Behavior/LogsMessagesTrait.php +++ b/src/API/Behavior/LogsMessagesTrait.php @@ -2,16 +2,15 @@ declare(strict_types=1); -namespace OpenTelemetry\SDK\Behavior; +namespace OpenTelemetry\API\Behavior; -use OpenTelemetry\SDK\Common\Log\LoggerHolder; +use OpenTelemetry\API\Common\Log\LoggerHolder; use Psr\Log\LogLevel; trait LogsMessagesTrait { private static function doLog(string $level, string $message, array $context): void { - $context['source'] = get_called_class(); LoggerHolder::get()->log($level, $message, $context); } diff --git a/src/API/Common/Event/Dispatcher.php b/src/API/Common/Event/Dispatcher.php new file mode 100644 index 000000000..a07374850 --- /dev/null +++ b/src/API/Common/Event/Dispatcher.php @@ -0,0 +1,39 @@ +listen(EventType::ERROR, new ErrorEventHandler()); + $dispatcher->listen(EventType::WARNING, new WarningEventHandler()); + $dispatcher->listen(EventType::DEBUG, new DebugEventHandler()); + + self::$instance = $dispatcher; + } + + return self::$instance; + } + + public static function setInstance(EventDispatcherInterface $dispatcher): void + { + self::$instance = $dispatcher; + } + + public static function unset(): void + { + self::$instance = null; + } +} diff --git a/src/API/Common/Event/Event/DebugEvent.php b/src/API/Common/Event/Event/DebugEvent.php new file mode 100644 index 000000000..b2433dbe7 --- /dev/null +++ b/src/API/Common/Event/Event/DebugEvent.php @@ -0,0 +1,27 @@ +message = $message; + $this->extra = $extra; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getExtra(): array + { + return $this->extra; + } +} diff --git a/src/API/Common/Event/Event/ErrorEvent.php b/src/API/Common/Event/Event/ErrorEvent.php new file mode 100644 index 000000000..8d8fd6901 --- /dev/null +++ b/src/API/Common/Event/Event/ErrorEvent.php @@ -0,0 +1,33 @@ +message = $message; + $this->exception = $error; + } + + public function getException(): Throwable + { + return $this->exception; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/API/Common/Event/Event/WarningEvent.php b/src/API/Common/Event/Event/WarningEvent.php new file mode 100644 index 000000000..95437187f --- /dev/null +++ b/src/API/Common/Event/Event/WarningEvent.php @@ -0,0 +1,38 @@ +message = $message; + $this->exception = $exception; + } + + public function getException(): ?Throwable + { + return $this->exception; + } + + public function hasError(): bool + { + return $this->exception !== null; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/API/Common/Event/EventType.php b/src/API/Common/Event/EventType.php new file mode 100644 index 000000000..bc0abda2f --- /dev/null +++ b/src/API/Common/Event/EventType.php @@ -0,0 +1,16 @@ +getMessage(), $event->getExtra()); + } +} diff --git a/src/API/Common/Event/Handler/ErrorEventHandler.php b/src/API/Common/Event/Handler/ErrorEventHandler.php new file mode 100644 index 000000000..6cd00b2cb --- /dev/null +++ b/src/API/Common/Event/Handler/ErrorEventHandler.php @@ -0,0 +1,18 @@ +getMessage(), ['exception' => $event->getException()]); + } +} diff --git a/src/API/Common/Event/Handler/WarningEventHandler.php b/src/API/Common/Event/Handler/WarningEventHandler.php new file mode 100644 index 000000000..ce552ecf6 --- /dev/null +++ b/src/API/Common/Event/Handler/WarningEventHandler.php @@ -0,0 +1,18 @@ +getMessage(), ['exception' => $event->getException()]); + } +} diff --git a/src/API/Common/Event/SimpleDispatcher.php b/src/API/Common/Event/SimpleDispatcher.php new file mode 100644 index 000000000..93cd066e9 --- /dev/null +++ b/src/API/Common/Event/SimpleDispatcher.php @@ -0,0 +1,62 @@ +listenerProvider = $listenerProvider; + } + + public function getListenerProvider(): ListenerProviderInterface + { + return $this->listenerProvider; + } + + public function listen(string $event, callable $listener, int $priority = 0): void + { + if (is_a($this->listenerProvider, SimpleListenerProvider::class)) { + $this->listenerProvider->listen($event, $listener, $priority); + } + /* there is no standard interface to register listeners, nor access a listener provider. Using a different listener provider + requires also setting up all required listeners for that provider */ + } + + public function dispatch(object $event): object + { + $listeners = $this->listenerProvider->getListenersForEvent($event); + + $event instanceof StoppableEventInterface + ? $this->dispatchStoppableEvent($listeners, $event) + : $this->dispatchEvent($listeners, $event); + + return $event; + } + + private function dispatchStoppableEvent(iterable $listeners, StoppableEventInterface $event): void + { + foreach ($listeners as $listener) { + if ($event->isPropagationStopped()) { + break; + } + + $listener($event); + } + } + + private function dispatchEvent(iterable $listeners, object $event): void + { + foreach ($listeners as $listener) { + $listener($event); + } + } +} diff --git a/src/API/Common/Event/SimpleListenerProvider.php b/src/API/Common/Event/SimpleListenerProvider.php new file mode 100644 index 000000000..7f82f0030 --- /dev/null +++ b/src/API/Common/Event/SimpleListenerProvider.php @@ -0,0 +1,35 @@ +>> */ + private array $listeners = []; + + /** + * @psalm-suppress ArgumentTypeCoercion + */ + public function getListenersForEvent(object $event): iterable + { + foreach ($this->listeners as $key => $priority) { + if (is_a($event, $key)) { + foreach ($priority as $listeners) { + foreach ($listeners as $listener) { + yield $listener; + } + } + } + } + } + + public function listen(string $event, callable $listener, int $priority = 0): void + { + $this->listeners[$event][$priority][] = $listener; + ksort($this->listeners[$event]); + } +} diff --git a/src/API/Common/Event/StoppableEventTrait.php b/src/API/Common/Event/StoppableEventTrait.php new file mode 100644 index 000000000..881397dd3 --- /dev/null +++ b/src/API/Common/Event/StoppableEventTrait.php @@ -0,0 +1,20 @@ +stopped; + } + + public function stopPropagation(): void + { + $this->stopped = true; + } +} diff --git a/src/SDK/Common/Log/LoggerHolder.php b/src/API/Common/Log/LoggerHolder.php similarity index 96% rename from src/SDK/Common/Log/LoggerHolder.php rename to src/API/Common/Log/LoggerHolder.php index 1a91451e1..c39214b04 100644 --- a/src/SDK/Common/Log/LoggerHolder.php +++ b/src/API/Common/Log/LoggerHolder.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace OpenTelemetry\SDK\Common\Log; +namespace OpenTelemetry\API\Common\Log; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; diff --git a/src/API/composer.json b/src/API/composer.json index 2f32e8d9f..979c7abfc 100644 --- a/src/API/composer.json +++ b/src/API/composer.json @@ -13,7 +13,9 @@ "require": { "php": "^7.4 || ^8.0", "open-telemetry/context": "self.version", - "open-telemetry/sem-conv": "self.version" + "open-telemetry/sem-conv": "self.version", + "psr/event-dispatcher": "^1", + "psr/log": "^1.1|^2.0|^3.0" }, "autoload": { "psr-4": { diff --git a/src/Contrib/Jaeger/HttpSender.php b/src/Contrib/Jaeger/HttpSender.php index 60d8e5bb3..87426d5d6 100644 --- a/src/Contrib/Jaeger/HttpSender.php +++ b/src/Contrib/Jaeger/HttpSender.php @@ -9,7 +9,6 @@ use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterFactoryInterface; use OpenTelemetry\Contrib\Jaeger\BatchAdapter\BatchAdapterInterface; use OpenTelemetry\Contrib\Jaeger\TagFactory\TagFactory; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; use OpenTelemetry\SDK\Resource\ResourceInfo; use OpenTelemetry\SemConv\ResourceAttributes; use Psr\Http\Client\ClientInterface; @@ -20,8 +19,6 @@ class HttpSender { - use LogsMessagesTrait; - private string $serviceName; private TProtocol $protocol; diff --git a/src/Contrib/Jaeger/JaegerTransport.php b/src/Contrib/Jaeger/JaegerTransport.php index 786d20ae3..431fd82ed 100644 --- a/src/Contrib/Jaeger/JaegerTransport.php +++ b/src/Contrib/Jaeger/JaegerTransport.php @@ -8,13 +8,14 @@ use Jaeger\Thrift\Batch; use Jaeger\Thrift\Process; use Jaeger\Thrift\Span; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; +use OpenTelemetry\API\Behavior\EmitsEventsTrait; +use OpenTelemetry\API\Common\Event\Event\ErrorEvent; use Thrift\Exception\TTransportException; use Thrift\Protocol\TCompactProtocol; final class JaegerTransport implements TransportInterface { - use LogsMessagesTrait; + use EmitsEventsTrait; // DEFAULT_BUFFER_SIZE indicates the default maximum buffer size, or the size threshold // at which the buffer will be flushed to the agent. @@ -93,7 +94,7 @@ public function flush($force = false): int // reset the process tag $this->process = null; } catch (TTransportException $e) { - self::logError('jaeger: transport failure: ' . $e->getMessage()); + self::emit(new ErrorEvent('jaeger transport failure', $e)); return 0; } diff --git a/src/Contrib/Otlp/ExporterTrait.php b/src/Contrib/Otlp/ExporterTrait.php index 4f279f852..26d477bd5 100644 --- a/src/Contrib/Otlp/ExporterTrait.php +++ b/src/Contrib/Otlp/ExporterTrait.php @@ -4,13 +4,11 @@ namespace OpenTelemetry\Contrib\Otlp; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; use OpenTelemetry\SDK\Common\Environment\EnvironmentVariablesTrait; use OpenTelemetry\SDK\Trace\Behavior\UsesSpanConverterTrait; trait ExporterTrait { use EnvironmentVariablesTrait; - use LogsMessagesTrait; use UsesSpanConverterTrait; } diff --git a/src/Contrib/OtlpGrpc/Exporter.php b/src/Contrib/OtlpGrpc/Exporter.php index 6687d2ae2..3a1c72625 100644 --- a/src/Contrib/OtlpGrpc/Exporter.php +++ b/src/Contrib/OtlpGrpc/Exporter.php @@ -4,8 +4,13 @@ namespace OpenTelemetry\Contrib\OtlpGrpc; +use Exception; use grpc; use Grpc\ChannelCredentials; +use OpenTelemetry\API\Behavior\EmitsEventsTrait; +use OpenTelemetry\API\Common\Event\Event\DebugEvent; +use OpenTelemetry\API\Common\Event\Event\ErrorEvent; +use OpenTelemetry\API\Common\Event\Event\WarningEvent; use OpenTelemetry\Contrib\Otlp\ExporterTrait; use OpenTelemetry\Contrib\Otlp\SpanConverter; use Opentelemetry\Proto\Collector\Trace\V1\ExportTraceServiceRequest; @@ -17,6 +22,7 @@ class Exporter implements SpanExporterInterface { + use EmitsEventsTrait; use ExporterTrait; use SpanExporterTrait; @@ -119,7 +125,7 @@ protected function doExport(iterable $spans): int [$response, $status] = $this->client->Export($request)->wait(); if ($status->code === \Grpc\STATUS_OK) { - self::logDebug('Exported span(s)', ['spans' => $resourceSpans]); + self::emit(new DebugEvent('Exported span(s)', ['spans' => $resourceSpans])); return self::STATUS_SUCCESS; } @@ -140,12 +146,12 @@ protected function doExport(iterable $spans): int \Grpc\STATUS_DATA_LOSS, \Grpc\STATUS_UNAUTHENTICATED, ], true)) { - self::logWarning('Retryable error exporting grpc span', ['error' => $error]); + self::emit(new WarningEvent('Retryable error exporting grpc span', new Exception($error['error'], $error['code']))); return self::STATUS_FAILED_RETRYABLE; } - self::logError('Error exporting grpc span', ['error' => $error]); + self::emit(new ErrorEvent('Error exporting grpc span', new Exception($error['error']))); return self::STATUS_FAILED_NOT_RETRYABLE; } diff --git a/src/SDK/Common/Dev/Compatibility/BC/GlobalLoggerHolder.php b/src/SDK/Common/Dev/Compatibility/BC/GlobalLoggerHolder.php index 2edf1c2ed..3189f87a8 100644 --- a/src/SDK/Common/Dev/Compatibility/BC/GlobalLoggerHolder.php +++ b/src/SDK/Common/Dev/Compatibility/BC/GlobalLoggerHolder.php @@ -4,8 +4,8 @@ namespace OpenTelemetry\SDK\Common\Dev\Compatibility\BC; +use OpenTelemetry\API\Common\Log\LoggerHolder as Moved; use OpenTelemetry\SDK\Common\Dev\Compatibility\Util; -use OpenTelemetry\SDK\Common\Log\LoggerHolder as Moved; use Psr\Log\LoggerInterface; /** diff --git a/src/SDK/Common/Dev/Compatibility/BC/LoggerHolder.php b/src/SDK/Common/Dev/Compatibility/BC/LoggerHolder.php new file mode 100644 index 000000000..492394787 --- /dev/null +++ b/src/SDK/Common/Dev/Compatibility/BC/LoggerHolder.php @@ -0,0 +1,65 @@ +dispatchSpans($spans); } catch (ClientExceptionInterface $e) { + self::emit(new ErrorEvent('Error exporting span', $e)); + return $e instanceof RequestExceptionInterface ? SpanExporterInterface::STATUS_FAILED_NOT_RETRYABLE : SpanExporterInterface::STATUS_FAILED_RETRYABLE; } catch (Throwable $e) { + self::emit(new ErrorEvent('Error exporting span', $e)); + return SpanExporterInterface::STATUS_FAILED_NOT_RETRYABLE; } if ($response->getStatusCode() >= 400) { + self::emit(new ErrorEvent('40x error exporting span', new Exception('', $response->getStatusCode()))); + return $response->getStatusCode() < 500 ? SpanExporterInterface::STATUS_FAILED_NOT_RETRYABLE : SpanExporterInterface::STATUS_FAILED_RETRYABLE; diff --git a/src/SDK/Trace/SpanProcessor/SimpleSpanProcessor.php b/src/SDK/Trace/SpanProcessor/SimpleSpanProcessor.php index 106a1beeb..63b7884aa 100644 --- a/src/SDK/Trace/SpanProcessor/SimpleSpanProcessor.php +++ b/src/SDK/Trace/SpanProcessor/SimpleSpanProcessor.php @@ -4,8 +4,9 @@ namespace OpenTelemetry\SDK\Trace\SpanProcessor; +use OpenTelemetry\API\Behavior\EmitsEventsTrait; +use OpenTelemetry\API\Common\Event\Event\DebugEvent; use OpenTelemetry\Context\Context; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; use OpenTelemetry\SDK\Trace\ReadableSpanInterface; use OpenTelemetry\SDK\Trace\ReadWriteSpanInterface; use OpenTelemetry\SDK\Trace\SpanExporterInterface; @@ -13,7 +14,7 @@ class SimpleSpanProcessor implements SpanProcessorInterface { - use LogsMessagesTrait; + use EmitsEventsTrait; private ?SpanExporterInterface $exporter; private bool $running = true; @@ -54,7 +55,7 @@ public function shutdown(): bool } $this->running = false; - self::logDebug('Shutting down span processor'); + self::emit(new DebugEvent('Shutting down span processor')); if (null !== $this->exporter) { return $this->forceFlush() && $this->exporter->shutdown(); diff --git a/src/SDK/Trace/TracerProviderFactory.php b/src/SDK/Trace/TracerProviderFactory.php index f27410eaa..095bb0ef3 100644 --- a/src/SDK/Trace/TracerProviderFactory.php +++ b/src/SDK/Trace/TracerProviderFactory.php @@ -4,12 +4,13 @@ namespace OpenTelemetry\SDK\Trace; +use OpenTelemetry\API\Behavior\EmitsEventsTrait; +use OpenTelemetry\API\Common\Event\Event\WarningEvent; use OpenTelemetry\API\Trace as API; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; final class TracerProviderFactory { - use LogsMessagesTrait; + use EmitsEventsTrait; private ExporterFactory $exporterFactory; private SamplerFactory $samplerFactory; @@ -31,21 +32,21 @@ public function create(): API\TracerProviderInterface try { $exporter = $this->exporterFactory->fromEnvironment(); } catch (\Throwable $t) { - self::logWarning('Unable to create exporter', ['error' => $t]); + self::emit(new WarningEvent('Unable to create exporter', $t)); $exporter = null; } try { $sampler = $this->samplerFactory->fromEnvironment(); } catch (\Throwable $t) { - self::logWarning('Unable to create sampler', ['error' => $t]); + self::emit(new WarningEvent('Unable to create sampler', $t)); $sampler = null; } try { $spanProcessor = $this->spanProcessorFactory->fromEnvironment($exporter); } catch (\Throwable $t) { - self::logWarning('Unable to create span processor', ['error' => $t]); + self::emit(new WarningEvent('Unable to create span processor', $t)); $spanProcessor = null; } diff --git a/src/SDK/composer.json b/src/SDK/composer.json index 4cdf06418..3582ac2b8 100644 --- a/src/SDK/composer.json +++ b/src/SDK/composer.json @@ -20,7 +20,6 @@ "php-http/async-client-implementation": "^1.0", "php-http/discovery": "^1.14", "psr/http-factory-implementation": "^1.0", - "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-mbstring": "^1.23" }, "autoload": { diff --git a/tests/Benchmark/EventBench.php b/tests/Benchmark/EventBench.php new file mode 100644 index 000000000..ecc9b92c7 --- /dev/null +++ b/tests/Benchmark/EventBench.php @@ -0,0 +1,80 @@ +listenerProvider = new SimpleListenerProvider(); + $this->dispatcher = new SimpleDispatcher($this->listenerProvider); + $this->listener = function () { + }; + $this->event = new stdClass(); + } + + public function addEventsToListener(): void + { + for ($i=0; $i<10; $i++) { + $this->listenerProvider->listen('event_' . $i, $this->listener); + } + $this->listenerProvider->listen(get_class($this->event), $this->listener); + } + + /** + * @ParamProviders("provideListenerCounts") + * @Revs(1000) + * @Iterations(10) + * @OutputTimeUnit("microseconds") + */ + public function benchAddListeners(array $params): void + { + for ($i=0; $i<$params[0]; $i++) { + $this->listenerProvider->listen('event_' . $i, $this->listener); + } + } + + /** + * @ParamProviders("provideListenerCounts") + * @Revs(1000) + * @Iterations(10) + * @OutputTimeUnit("microseconds") + */ + public function benchAddListenersForSameEvent(array $params): void + { + for ($i=0; $i<$params[0]; $i++) { + $this->listenerProvider->listen('event', $this->listener); + } + } + + /** + * @BeforeMethods("addEventsToListener") + * @Revs(1000) + * @Iterations(10) + * @OutputTimeUnit("microseconds") + */ + public function benchDispatchEvent(): void + { + $this->dispatcher->dispatch($this->event); + } + + public function provideListenerCounts(): Generator + { + yield [1]; + yield [4]; + yield [16]; + yield [256]; + } +} diff --git a/tests/Unit/SDK/Behavior/LogsMessagesTraitTest.php b/tests/Unit/API/Behavior/LogsMessagesTraitTest.php similarity index 89% rename from tests/Unit/SDK/Behavior/LogsMessagesTraitTest.php rename to tests/Unit/API/Behavior/LogsMessagesTraitTest.php index 2d55b844c..8753e70d8 100644 --- a/tests/Unit/SDK/Behavior/LogsMessagesTraitTest.php +++ b/tests/Unit/API/Behavior/LogsMessagesTraitTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace OpenTelemetry\Tests\Unit\SDK\Behavior; +namespace OpenTelemetry\Tests\Unit\API\Behavior; -use OpenTelemetry\SDK\Behavior\LogsMessagesTrait; -use OpenTelemetry\SDK\Common\Log\LoggerHolder; +use OpenTelemetry\API\Behavior\LogsMessagesTrait; +use OpenTelemetry\API\Common\Log\LoggerHolder; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** - * @covers \OpenTelemetry\SDK\Behavior\LogsMessagesTrait + * @covers \OpenTelemetry\API\Behavior\LogsMessagesTrait */ class LogsMessagesTraitTest extends TestCase { diff --git a/tests/Unit/API/Common/Event/DispatcherTest.php b/tests/Unit/API/Common/Event/DispatcherTest.php new file mode 100644 index 000000000..135ae719b --- /dev/null +++ b/tests/Unit/API/Common/Event/DispatcherTest.php @@ -0,0 +1,47 @@ +assertInstanceOf(SimpleDispatcher::class, $dispatcher); + $this->assertInstanceOf(SimpleListenerProvider::class, $dispatcher->getListenerProvider()); + $this->assertGreaterThan(0, count([...$dispatcher->getListenerProvider()->getListenersForEvent($this->createMock(ErrorEvent::class))])); + $this->assertGreaterThan(0, count([...$dispatcher->getListenerProvider()->getListenersForEvent($this->createMock(WarningEvent::class))])); + $this->assertGreaterThan(0, count([...$dispatcher->getListenerProvider()->getListenersForEvent($this->createMock(DebugEvent::class))])); + } + + public function test_set_instance(): void + { + $mock = $this->createMock(EventDispatcherInterface::class); + Dispatcher::setInstance($mock); + $this->assertSame($mock, Dispatcher::getInstance()); + } +} diff --git a/tests/Unit/API/Common/Event/Event/DebugEventTest.php b/tests/Unit/API/Common/Event/Event/DebugEventTest.php new file mode 100644 index 000000000..9c28543c9 --- /dev/null +++ b/tests/Unit/API/Common/Event/Event/DebugEventTest.php @@ -0,0 +1,23 @@ + 'baz']; + $event = new DebugEvent($message, $extra); + $this->assertSame($message, $event->getMessage()); + $this->assertSame($extra, $event->getExtra()); + } +} diff --git a/tests/Unit/API/Common/Event/Event/ErrorEventTest.php b/tests/Unit/API/Common/Event/Event/ErrorEventTest.php new file mode 100644 index 000000000..5c2693711 --- /dev/null +++ b/tests/Unit/API/Common/Event/Event/ErrorEventTest.php @@ -0,0 +1,22 @@ +assertSame($message, $event->getMessage()); + $this->assertSame($exception, $event->getException()); + } +} diff --git a/tests/Unit/API/Common/Event/Event/WarningEventTest.php b/tests/Unit/API/Common/Event/Event/WarningEventTest.php new file mode 100644 index 000000000..30cf392d5 --- /dev/null +++ b/tests/Unit/API/Common/Event/Event/WarningEventTest.php @@ -0,0 +1,33 @@ +assertSame($message, $event->getMessage()); + $this->assertTrue($event->hasError()); + $this->assertSame($exception, $event->getException()); + } + + public function test_warning_event_without_throwable(): void + { + $message = 'foo'; + $event = new WarningEvent($message); + $this->assertSame($message, $event->getMessage()); + $this->assertFalse($event->hasError()); + $this->assertNull($event->getException()); + } +} diff --git a/tests/Unit/API/Common/Event/Handler/DebugHandlerTest.php b/tests/Unit/API/Common/Event/Handler/DebugHandlerTest.php new file mode 100644 index 000000000..a31eaf214 --- /dev/null +++ b/tests/Unit/API/Common/Event/Handler/DebugHandlerTest.php @@ -0,0 +1,26 @@ +logger->expects($this->once())->method('log')->with($this->equalTo(LogLevel::DEBUG)); + $handler($event); + } +} diff --git a/tests/Unit/API/Common/Event/Handler/ErrorHandlerTest.php b/tests/Unit/API/Common/Event/Handler/ErrorHandlerTest.php new file mode 100644 index 000000000..fd23ceaac --- /dev/null +++ b/tests/Unit/API/Common/Event/Handler/ErrorHandlerTest.php @@ -0,0 +1,28 @@ +logger->expects($this->once())->method('log')->with($this->equalTo(LogLevel::ERROR)); + $handler($event); + } +} diff --git a/tests/Unit/API/Common/Event/Handler/LogBasedHandlerTest.php b/tests/Unit/API/Common/Event/Handler/LogBasedHandlerTest.php new file mode 100644 index 000000000..7cb3dd0d6 --- /dev/null +++ b/tests/Unit/API/Common/Event/Handler/LogBasedHandlerTest.php @@ -0,0 +1,26 @@ +logger = $this->createMock(LoggerInterface::class); + LoggerHolder::set($this->logger); + } + + public function tearDown(): void + { + LoggerHolder::unset(); + } +} diff --git a/tests/Unit/API/Common/Event/Handler/WarningHandlerTest.php b/tests/Unit/API/Common/Event/Handler/WarningHandlerTest.php new file mode 100644 index 000000000..0fa39b71b --- /dev/null +++ b/tests/Unit/API/Common/Event/Handler/WarningHandlerTest.php @@ -0,0 +1,27 @@ +logger->expects($this->once())->method('log')->with($this->equalTo(LogLevel::WARNING)); + $handler($event); + } +} diff --git a/tests/Unit/API/Common/Event/SimpleDispatcherTest.php b/tests/Unit/API/Common/Event/SimpleDispatcherTest.php new file mode 100644 index 000000000..4e1927bd4 --- /dev/null +++ b/tests/Unit/API/Common/Event/SimpleDispatcherTest.php @@ -0,0 +1,92 @@ +listenerProvider = $this->createMock(ListenerProviderInterface::class); + } + + public function test_get_listener_provider(): void + { + $dispatcher = new SimpleDispatcher($this->listenerProvider); + $this->assertSame($this->listenerProvider, $dispatcher->getListenerProvider()); + } + + public function test_proxies_listen_for_simple_listener_provider(): void + { + $provider = $this->createMock(\OpenTelemetry\API\Common\Event\SimpleListenerProvider::class); + $callable = function () { + }; + $eventName = 'my.event'; + $priority = 99; + $dispatcher = new SimpleDispatcher($provider); + $provider + ->expects($this->once()) + ->method('listen') + ->with( + $this->equalTo($eventName), + $this->equalTo($callable), + $this->equalTo($priority) + ); + $dispatcher->listen($eventName, $callable, $priority); + } + + /** + * @doesNotPerformAssertions + */ + public function test_skips_other_listener_providers(): void + { + $provider = $this->createMock(ListenerProviderInterface::class); + $dispatcher = new SimpleDispatcher($provider); + $dispatcher->listen('my.event', function () { + }); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod + */ + public function test_dispatch_event(): void + { + $event = new stdClass(); + $handler = function ($receivedEvent) use ($event) { + $this->assertSame($event, $receivedEvent); + }; + $this->listenerProvider->expects($this->once())->method('getListenersForEvent')->willReturn([$handler]); //@phpstan-ignore-line + $dispatcher = new \OpenTelemetry\API\Common\Event\SimpleDispatcher($this->listenerProvider); + $dispatcher->dispatch($event); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod + */ + public function test_dispatch_stoppable_event(): void + { + $event = $this->createMock(StoppableEventInterface::class); + $event->method('isPropagationStopped')->willReturnOnConsecutiveCalls(false, true); + $handlerOne = function (StoppableEventInterface $event) { + $this->assertTrue(true, 'handler called'); + }; + $handlerTwo = function (StoppableEventInterface $event) { + $this->fail('method should not have been called'); + }; + $this->listenerProvider->method('getListenersForEvent')->willReturn([$handlerOne, $handlerTwo]); //@phpstan-ignore-line + $dispatcher = new SimpleDispatcher($this->listenerProvider); + $dispatcher->dispatch($event); + } +} diff --git a/tests/Unit/API/Common/Event/SimpleListenerProviderTest.php b/tests/Unit/API/Common/Event/SimpleListenerProviderTest.php new file mode 100644 index 000000000..f51557a5e --- /dev/null +++ b/tests/Unit/API/Common/Event/SimpleListenerProviderTest.php @@ -0,0 +1,82 @@ +provider = new \OpenTelemetry\API\Common\Event\SimpleListenerProvider(); + } + + public function test_add_listeners(): void + { + $event = new stdClass(); + $listenerFunction = function () { + }; + $this->provider->listen(get_class($event), $listenerFunction); + $listeners = [...$this->provider->getListenersForEvent($event)]; + $this->assertCount(1, $listeners); + $this->assertSame($listenerFunction, $listeners[0]); + } + + public function test_can_add_multiple_listeners_with_same_priority(): void + { + $event = new stdClass(); + $listenerOne = function ($event) { + }; + $listenerTwo = function ($event) { + }; + $this->provider->listen(get_class($event), $listenerOne); + $this->provider->listen(get_class($event), $listenerTwo); + $listeners = [...$this->provider->getListenersForEvent($event)]; + $this->assertCount(2, $listeners); + $this->assertSame($listenerOne, $listeners[0]); + $this->assertSame($listenerTwo, $listeners[1]); + } + + public function test_listener_priority(): void + { + $event = new stdClass(); + $listenerOne = function () { + }; + $listenerTwo = function () { + }; + $listenerThree = function () { + }; + $listenerFour = function () { + }; + $this->provider->listen(get_class($event), $listenerOne, 1); + $this->provider->listen(get_class($event), $listenerTwo, -1); + $this->provider->listen(get_class($event), $listenerThree, 0); + $this->provider->listen(get_class($event), $listenerFour, 1); + $listeners = [...$this->provider->getListenersForEvent($event)]; + $this->assertCount(4, $listeners); + $this->assertSame($listenerTwo, $listeners[0]); + $this->assertSame($listenerThree, $listeners[1]); + $this->assertSame($listenerOne, $listeners[2]); + $this->assertSame($listenerFour, $listeners[3]); + } + + public function test_get_listener_for_subclass(): void + { + $event = new stdClass(); + $subclass = $this->createMock(stdClass::class); + $listener = function () { + }; + $this->provider->listen(stdClass::class, $listener); + $listeners = [...$this->provider->getListenersForEvent($subclass)]; + $this->assertCount(1, $listeners); + $this->assertSame($listener, $listeners[0]); + } +} diff --git a/tests/Unit/API/Common/Event/StoppableEventTraitTest.php b/tests/Unit/API/Common/Event/StoppableEventTraitTest.php new file mode 100644 index 000000000..d08f95101 --- /dev/null +++ b/tests/Unit/API/Common/Event/StoppableEventTraitTest.php @@ -0,0 +1,32 @@ +getEvent(); + $this->assertFalse($event->isPropagationStopped()); + $event->stopPropagation(); //@phpstan-ignore-line + $this->assertTrue($event->isPropagationStopped()); + } + + public function getEvent(): StoppableEventInterface + { + return new class() implements StoppableEventInterface { + use \OpenTelemetry\API\Common\Event\StoppableEventTrait; + }; + } +} diff --git a/tests/Unit/SDK/Common/Log/LoggerHolderTest.php b/tests/Unit/SDK/Common/Log/LoggerHolderTest.php index d19801818..26c78886b 100644 --- a/tests/Unit/SDK/Common/Log/LoggerHolderTest.php +++ b/tests/Unit/SDK/Common/Log/LoggerHolderTest.php @@ -4,13 +4,13 @@ namespace OpenTelemetry\Tests\Unit\SDK\Common\Log; -use OpenTelemetry\SDK\Common\Log\LoggerHolder; +use OpenTelemetry\API\Common\Log\LoggerHolder; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; /** - * @covers \OpenTelemetry\SDK\Common\Log\LoggerHolder + * @covers \OpenTelemetry\API\Common\Log\LoggerHolder */ class LoggerHolderTest extends TestCase { diff --git a/tests/Unit/SDK/Trace/Behavior/HttpSpanExporterTraitTest.php b/tests/Unit/SDK/Trace/Behavior/HttpSpanExporterTraitTest.php new file mode 100644 index 000000000..ee92c4c55 --- /dev/null +++ b/tests/Unit/SDK/Trace/Behavior/HttpSpanExporterTraitTest.php @@ -0,0 +1,93 @@ +dispatcher = $this->createMock(EventDispatcherInterface::class); + $request = $this->createMock(RequestInterface::class); + $this->client = $this->createMock(ClientInterface::class); + $this->exporter = $this->createExporter(); + $this->exporter->setRequest($request); //@phpstan-ignore-line + $this->exporter->setClient($this->client); //@phpstan-ignore-line + Dispatcher::setInstance($this->dispatcher); + } + + public function tearDown(): void + { + Dispatcher::unset(); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod + */ + public function test_export_with_client_exception_generates_error_event(): void + { + $this->client->expects($this->once())->method('sendRequest')->willThrowException($this->createMock(ClientException::class)); //@phpstan-ignore-line + $this->dispatcher->expects($this->once())->method('dispatch'); //@phpstan-ignore-line + $this->exporter->export([$this->createMock(SpanDataInterface::class)]); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod + */ + public function test_export_with_throwable_generates_error_event(): void + { + $this->client->expects($this->once())->method('sendRequest')->willThrowException($this->createMock(Throwable::class)); //@phpstan-ignore-line + $this->dispatcher->expects($this->once())->method('dispatch'); //@phpstan-ignore-line + $this->exporter->export([$this->createMock(SpanDataInterface::class)]); + } + + private function createExporter(): SpanExporterInterface + { + return new class() implements SpanExporterInterface { + use HttpSpanExporterTrait; + private RequestInterface $request; + public function setRequest(RequestInterface $request): void + { + $this->request = $request; + } + public function setClient(ClientInterface $client): void + { + $this->client = $client; + } + public function serializeTrace(iterable $spans): string + { + return ''; + } + public static function fromConnectionString(string $endpointUrl, string $name, string $args) + { + return new \stdClass(); + } + public function marshallRequest(iterable $spans): RequestInterface + { + return $this->request; + } + }; + } +} diff --git a/tests/Unit/SDK/Trace/SpanLimitsTest.php b/tests/Unit/SDK/Trace/SpanLimitsTest.php new file mode 100644 index 000000000..adcf93a33 --- /dev/null +++ b/tests/Unit/SDK/Trace/SpanLimitsTest.php @@ -0,0 +1,25 @@ +assertSame(1, $spanLimits->getAttributeLimits()->getAttributeCountLimit()); + $this->assertSame(2, $spanLimits->getAttributeLimits()->getAttributeValueLengthLimit()); + $this->assertSame(3, $spanLimits->getEventCountLimit()); + $this->assertSame(4, $spanLimits->getLinkCountLimit()); + $this->assertSame(5, $spanLimits->getAttributePerEventCountLimit()); + $this->assertSame(6, $spanLimits->getAttributePerLinkCountLimit()); + } +} diff --git a/tests/Unit/SDK/Trace/SpanTest.php b/tests/Unit/SDK/Trace/SpanTest.php index 1d551086f..cef953d4a 100644 --- a/tests/Unit/SDK/Trace/SpanTest.php +++ b/tests/Unit/SDK/Trace/SpanTest.php @@ -41,6 +41,7 @@ /** * @covers OpenTelemetry\SDK\Trace\Span + * @coversDefaultClass \OpenTelemetry\SDK\Trace\Span */ class SpanTest extends MockeryTestCase { diff --git a/tests/Unit/SDK/Trace/TracerProviderFactoryTest.php b/tests/Unit/SDK/Trace/TracerProviderFactoryTest.php index 67ca69a20..002b9063a 100644 --- a/tests/Unit/SDK/Trace/TracerProviderFactoryTest.php +++ b/tests/Unit/SDK/Trace/TracerProviderFactoryTest.php @@ -4,25 +4,27 @@ namespace OpenTelemetry\Tests\Unit\SDK\Trace; -use OpenTelemetry\SDK\Common\Log\LoggerHolder; +use OpenTelemetry\API\Common\Event\Dispatcher; +use OpenTelemetry\API\Common\Event\Event\WarningEvent; +use OpenTelemetry\API\Common\Log\LoggerHolder; use OpenTelemetry\SDK\Trace\ExporterFactory; use OpenTelemetry\SDK\Trace\SamplerFactory; use OpenTelemetry\SDK\Trace\SpanProcessorFactory; use OpenTelemetry\SDK\Trace\TracerProviderFactory; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; /** * @coversDefaultClass \OpenTelemetry\SDK\Trace\TracerProviderFactory */ class TracerProviderFactoryTest extends TestCase { - private $logger; + private $dispatcher; public function setUp(): void { - $this->logger = $this->createMock(LoggerInterface::class); - LoggerHolder::set($this->logger); + $this->dispatcher = $this->createMock(EventDispatcherInterface::class); + Dispatcher::setInstance($this->dispatcher); } public function tearDown(): void @@ -51,7 +53,7 @@ public function test_factory_creates_tracer(): void /** * @covers ::create */ - public function test_factory_logs_warnings_and_continues(): void + public function test_factory_emits_warnings_and_continues(): void { $exporterFactory = $this->createMock(ExporterFactory::class); $samplerFactory = $this->createMock(SamplerFactory::class); @@ -66,7 +68,11 @@ public function test_factory_logs_warnings_and_continues(): void $spanProcessorFactory->expects($this->once()) ->method('fromEnvironment') ->willThrowException(new \InvalidArgumentException('foo')); - $this->logger->expects($this->atLeast(3))->method('log'); + $this->dispatcher->expects($this->atLeast(3))->method('dispatch')->with($this->callback(function ($event) { + $this->assertInstanceOf(WarningEvent::class, $event); + + return true; + })); $factory = new TracerProviderFactory('test', $exporterFactory, $samplerFactory, $spanProcessorFactory); $factory->create();