diff --git a/.gitsplit.yml b/.gitsplit.yml index 9f7eb8289..c57cb6d32 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -24,6 +24,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/transport-grpc.git" - prefix: "src/Contrib/Zipkin" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/exporter-zipkin.git" + - prefix: "src/Contrib/Instana" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/exporter-instana.git" - prefix: "src/Extension/Propagator/B3" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/extension-propagator-b3.git" - prefix: "src/Extension/Propagator/CloudTrace" diff --git a/composer.json b/composer.json index e127ff8fc..af0da00a9 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "open-telemetry/context": "1.0.x-dev", "open-telemetry/exporter-otlp": "1.0.x-dev", "open-telemetry/exporter-zipkin": "1.0.x-dev", + "open-telemetry/exporter-instana": "1.0.x-dev", "open-telemetry/extension-propagator-b3": "1.0.x-dev", "open-telemetry/extension-propagator-jaeger": "0.0.2", "open-telemetry/gen-otlp-protobuf": "1.0.x-dev", @@ -63,6 +64,7 @@ "src/Contrib/Otlp/_register.php", "src/Contrib/Grpc/_register.php", "src/Contrib/Zipkin/_register.php", + "src/Contrib/Instana/_register.php", "src/Extension/Propagator/B3/_register.php", "src/Extension/Propagator/CloudTrace/_register.php", "src/Extension/Propagator/Jaeger/_register.php", diff --git a/examples/traces/exporters/instana.php b/examples/traces/exporters/instana.php new file mode 100644 index 000000000..56a0bd4cf --- /dev/null +++ b/examples/traces/exporters/instana.php @@ -0,0 +1,49 @@ +create() + ) +); + +$tracer = $tracerProvider->getTracer('io.opentelemetry.contrib.php'); + +echo 'Starting Instana example'; + +$root = $span = $tracer->spanBuilder('root')->startSpan(); +$scope = $span->activate(); + +for ($i = 0; $i < 3; $i++) { + // start a span, register some events + $span = $tracer->spanBuilder('loop-' . $i)->startSpan(); + + $span->setAttribute('remote_ip', '1.2.3.4') + ->setAttribute('country', 'USA'); + + $span->addEvent('found_login' . $i, [ + 'id' => $i, + 'username' => 'otuser' . $i, + ]); + $span->addEvent('generated_session', [ + 'id' => md5((string) microtime(true)), + ]); + + $span->end(); +} +$scope->detach(); +$root->end(); +echo PHP_EOL . 'Instana example complete!'; + +echo PHP_EOL; +$tracerProvider->shutdown(); diff --git a/src/Contrib/Instana/InstanaTransport.php b/src/Contrib/Instana/InstanaTransport.php new file mode 100644 index 000000000..79e8a3f03 --- /dev/null +++ b/src/Contrib/Instana/InstanaTransport.php @@ -0,0 +1,217 @@ +headers += ['Content-Type' => self::CONTENT_TYPE]; + if ($timeout > 0.0) { + $this->headers += ['timeout' => $timeout]; + } + + $this->client = new Client(['base_uri' => $endpoint]); + + for ($attempt = 0; $attempt < self::ATTEMPTS && !$this->announce(); $attempt++) { + self::logDebug("Discovery request failed, attempt " . $attempt); + sleep(5); + } + + if (is_null($this->agent_uuid) || is_null($this->pid)) { + throw new Exception('Failed announcement in transport'); + } + } + + public function contentType(): string + { + return self::CONTENT_TYPE; + } + + public function send(string $payload, ?CancellationInterface $cancellation = null): FutureInterface + { + if ($this->closed) { + return new ErrorFuture(new BadMethodCallException('Transport closed')); + } + + $response = $this->sendPayload($payload); + + $code = $response->getStatusCode(); + if ($code != 204 && $code != 307) { + self::logDebug("Sending failed with code " . $code); + return new ErrorFuture(new RuntimeException('Payload failed to send with code ' . $code)); + } + + return new CompletedFuture('Payload successfully sent'); + } + + private function sendPayload(string $payload): ResponseInterface + { + return $this->client->sendRequest( + new Request( + method: 'POST', + uri: new Uri('/com.instana.plugin.php/traces.' . $this->pid), + headers: $this->headers, + body: $payload + ) + ); + } + + public function shutdown(?CancellationInterface $cancellation = null): bool + { + if ($this->closed) { + return false; + } + + return $this->closed = true; + } + + public function forceFlush(?CancellationInterface $cancellation = null): bool + { + return !$this->closed; + } + + private function announce(): bool + { + self::logDebug("Announcing to " . $this->endpoint); + + // Phase 1) Host lookup. + $response = $this->client->sendRequest( + new Request(method: 'GET', uri: new Uri('/'), headers: $this->headers) + ); + + $code = $response->getStatusCode(); + $msg = $response->getBody()->getContents(); + + if ($code != 200 && !array_key_exists('version', json_decode($msg, true))) { + self::LogError("Failed to lookup host. Received code " . $code . " with message: " . $msg); + $this->closed = true; + return false; + } + + self::logDebug("Phase 1 announcement response code " . $code); + + // Phase 2) Announcement. + $response = $this->client->sendRequest( + new Request( + method: 'PUT', + uri: new Uri('/com.instana.plugin.php.discovery'), + headers: $this->headers, + body: $this->getAnnouncementPayload() + ) + ); + + $code = $response->getStatusCode(); + $msg = $response->getBody()->getContents(); + + self::logDebug("Phase 2 announcement response code " . $code); + + if ($code < 200 || $code >= 300) { + self::LogError("Failed announcement. Received code " . $code . " with message: " . $msg); + $this->closed = true; + return false; + } + + $content = json_decode($msg, true); + if (!array_key_exists('pid', $content)) { + self::LogError("Failed to receive a pid from agent"); + $this->closed = true; + return false; + } + + $this->pid = $content['pid']; + $this->agent_uuid = $content['agentUuid']; + + // Optional values that we may receive from the agent. + if (array_key_exists('secrets', $content)) $this->secrets = $content['secrets']; + if (array_key_exists('tracing', $content)) $this->tracing = $content['tracing']; + + // Phase 3) Wait for the agent ready signal. + for ($retry = 0; $retry < 5; $retry++) { + if ($retry) self::logDebug("Agent not yet ready, attempt " . $retry); + + $response = $this->client->sendRequest( + new Request( + method: 'HEAD', + uri: new Uri('/com.instana.plugin.php.' . $this->pid), + headers: $this->headers + ) + ); + + $code = $response->getStatusCode(); + self::logDebug("Phase 3 announcement endpoint status " . $code); + if ($code >= 200 && $code < 300) { + $this->closed = false; + return true; + } + + sleep(1); + } + + $this->closed = true; + return false; + } + + private function getAnnouncementPayload(): string + { + $cmdline_args = file_get_contents("/proc/self/cmdline"); + $cmdline_args = explode("\0", $cmdline_args); + $cmdline_args = array_slice($cmdline_args, 1, count($cmdline_args) - 2); + + return json_encode(array( + "pid" => getmypid(), + "pidFromParentNS" => false, + "pidNamespace" => readlink("/proc/self/ns/pid"), + "name" => readlink("/proc/self/exe"), + "args" => $cmdline_args, + "cpuSetFileContent" => "/", + "fd" => null, + "inode" => null + )); + } + + public function getPid(): ?string + { + return is_null($this->pid) ? null : strval($this->pid); + } + + public function getUuid(): ?string + { + return $this->agent_uuid; + } +} diff --git a/src/Contrib/Instana/README.md b/src/Contrib/Instana/README.md new file mode 100644 index 000000000..7f1cdd844 --- /dev/null +++ b/src/Contrib/Instana/README.md @@ -0,0 +1,22 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/exporter-instana/releases) +[![Source](https://img.shields.io/badge/source-exporter--instana-green)](https://github.com/open-telemetry/opentelemetry-php/tree/main/src/Contrib/Instana) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php:exporter--instana-blue)](https://github.com/opentelemetry-php/exporter-instana) +[![Latest Version](http://poser.pugx.org/open-telemetry/exporter-instana/v/unstable)](https://packagist.org/packages/open-telemetry/exporter-instana/) +[![Stable](http://poser.pugx.org/open-telemetry/exporter-instana/v/stable)](https://packagist.org/packages/open-telemetry/exporter-instana/) + +# OpenTelemetry Instana Exporter + +Instana exporter for OpenTelemetry. + +## Documentation + +https://opentelemetry.io/docs/instrumentation/php/exporters/#instana + +## Usage + +See https://github.com/open-telemetry/opentelemetry-php/blob/main/examples/traces/exporters/instana.php + +## Contributing + +This repository is a read-only git subtree split. +To contribute, please see the main [OpenTelemetry PHP monorepo](https://github.com/open-telemetry/opentelemetry-php). diff --git a/src/Contrib/Instana/SpanConverter.php b/src/Contrib/Instana/SpanConverter.php new file mode 100644 index 000000000..75f6a088f --- /dev/null +++ b/src/Contrib/Instana/SpanConverter.php @@ -0,0 +1,175 @@ +convertSpan($span); + } + + return $aggregate; + } + + private function convertSpan(SpanDataInterface $span): array + { + $startTimestamp = self::nanosToMillis($span->getStartEpochNanos()); + $endTimestamp = self::nanosToMillis($span->getEndEpochNanos()); + + if (is_null($this->agentUuid) || is_null($this->agentPid)) { + throw new Exception('Failed to get agentUuid or agentPid'); + } + + $instanaSpan = [ + 'f' => array('e' => $this->agentPid, 'h' => $this->agentUuid), + 's' => $span->getSpanId(), + 't' => $span->getTraceId(), + 'ts' => $startTimestamp, + 'd' => max(0, $endTimestamp - $startTimestamp), + 'n' => $span->getName(), + 'data' => [] + ]; + + if ($span->getParentContext()->isValid()) { + $instanaSpan['p'] = $span->getParentSpanId(); + } + + $convertedKind = SpanConverter::toSpanKind($span); + if (!is_null($convertedKind)) { + $instanaSpan['k'] = $convertedKind; + } + + self::insertSpanData($instanaSpan['data'], $span->getAttributes()); + self::insertSpanData($instanaSpan['data'], $span->getResource()->getAttributes()); + if (array_key_exists('service', $instanaSpan['data'])) { + self::setOrAppend('otel', $instanaSpan['data'], array('service' => $instanaSpan['data']['service'])); + } + $instanaSpan['data']['service'] = $span->getName(); + + self::insertSpanData($instanaSpan['data'], $span->getInstrumentationScope()->getAttributes()); + + if ($span->getStatus()->getCode() !== StatusCode::STATUS_UNSET) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_STATUS_CODE => $span->getStatus()->getCode())); + } + + if ($span->getStatus()->getCode() === StatusCode::STATUS_ERROR) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_STATUS_DESCRIPTION => $span->getStatus()->getDescription())); + } + + if (!empty($span->getInstrumentationScope()->getName())) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_INSTRUMENTATION_SCOPE_NAME => $span->getInstrumentationScope()->getName())); + } + + if ($span->getInstrumentationScope()->getVersion() !== null) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_INSTRUMENTATION_SCOPE_VERSION => $span->getInstrumentationScope()->getVersion())); + } + + foreach ($span->getEvents() as $event) { + self::setOrAppend('events', $instanaSpan['data'], array($event->getName() => self::convertEvent($event))); + } + + $droppedAttributes = $span->getAttributes()->getDroppedAttributesCount() + + $span->getInstrumentationScope()->getAttributes()->getDroppedAttributesCount() + + $span->getResource()->getAttributes()->getDroppedAttributesCount(); + + if ($droppedAttributes > 0) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_DROPPED_ATTRIBUTES_COUNT => $droppedAttributes)); + } + + if ($span->getTotalDroppedEvents() > 0) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_DROPPED_EVENTS_COUNT => $span->getTotalDroppedEvents())); + } + + if ($span->getTotalDroppedLinks() > 0) { + self::setOrAppend('otel', $instanaSpan['data'], array(self::OTEL_KEY_DROPPED_LINKS_COUNT => $span->getTotalDroppedLinks())); + } + + if (empty($instanaSpan['data'])) { + unset($instanaSpan['data']); + } + + return $instanaSpan; + } + + private static function toSpanKind(SpanDataInterface $span): ?int + { + return match ($span->getKind()) { + SpanKind::KIND_SERVER => InstanaSpanKind::ENTRY, + SpanKind::KIND_CLIENT => InstanaSpanKind::EXIT, + SpanKind::KIND_PRODUCER => InstanaSpanKind::EXIT, + SpanKind::KIND_CONSUMER => InstanaSpanKind::ENTRY, + SpanKind::KIND_INTERNAL => InstanaSpanKind::INTERMEDIATE, + default => null, + }; + } + + private static function nanosToMillis(int $nanoseconds): int + { + return intdiv($nanoseconds, ClockInterface::NANOS_PER_MILLISECOND); + } + + private static function insertSpanData(array &$data, AttributesInterface $attributes): void + { + foreach ($attributes as $key => $value) { + $arr = explode('.', $key, 2); + if (count($arr) < 2) { + $data += array($arr[0] => $value); + } else { + self::setOrAppend($arr[0], $data, array($arr[1] => $value)); + } + } + } + + private static function setOrAppend(string $key, array &$arr, array $value): void + { + if (array_key_exists($key, $arr)) { + $arr[$key] += $value; + } else { + $arr[$key] = $value; + } + } + + private static function convertEvent(EventInterface $event): string + { + if (count($event->getAttributes()) === 0) { + return "{}"; + } + + $res = json_encode(array( + 'value' => $event->getAttributes()->toArray(), + 'timestamp' => self::nanosToMillis($event->getEpochNanos()) + )); + + return ($res === false) ? "{}" : $res; + } +} diff --git a/src/Contrib/Instana/SpanExporter.php b/src/Contrib/Instana/SpanExporter.php new file mode 100644 index 000000000..210b3ba5e --- /dev/null +++ b/src/Contrib/Instana/SpanExporter.php @@ -0,0 +1,61 @@ +setSpanConverter($spanConverter); + } + + /** + * @throws JsonException + */ + protected function serializeTrace(iterable $spans): string + { + return json_encode( + $this->getSpanConverter()->convert($spans), + JSON_THROW_ON_ERROR + ); + } + + public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface + { + return $this->transport + ->send($this->serializeTrace($batch), $cancellation) + ->map(static fn(): bool => true) + ->catch(static function (Throwable $throwable): bool { + self::logError('Export failure', ['exception' => $throwable]); + + return false; + }); + } + + public function shutdown(?CancellationInterface $cancellation = null): bool + { + return $this->transport->shutdown($cancellation); + } + + public function forceFlush(?CancellationInterface $cancellation = null): bool + { + return $this->transport->forceFlush($cancellation); + } +} diff --git a/src/Contrib/Instana/SpanExporterFactory.php b/src/Contrib/Instana/SpanExporterFactory.php new file mode 100644 index 000000000..f96859db6 --- /dev/null +++ b/src/Contrib/Instana/SpanExporterFactory.php @@ -0,0 +1,27 @@ +getUuid(); + $pid = $transport->getPid(); + $converter = new SpanConverter($uuid, $pid); + + return new SpanExporter($transport, $converter); + } +} diff --git a/src/Contrib/Instana/SpanKind.php b/src/Contrib/Instana/SpanKind.php new file mode 100644 index 000000000..2bf4e5277 --- /dev/null +++ b/src/Contrib/Instana/SpanKind.php @@ -0,0 +1,12 @@ +setName('converter.test') + ->setKind(SpanKind::KIND_CLIENT) + ->setContext( + SpanContext::create( + 'abcdef0123456789abcdef0123456789', + 'aabbccddeeff0123' + ) + ) + ->setParentContext( + SpanContext::create( + '10000000000000000000000000000000', + '1000000000000000' + ) + ) + ->setStatus( + new StatusData( + StatusCode::STATUS_ERROR, + 'status_description' + ) + ) + ->setInstrumentationScope(new InstrumentationScope( + 'instrumentation_scope_name', + 'instrumentation_scope_version', + null, + Attributes::create([]), + )) + ->addAttribute('service', array('name' => 'unknown_service:php', 'version' => 'dev-main')) + ->addAttribute('net.peer.name', 'authorizationservice.com') + ->addAttribute('peer.service', 'AuthService') + ->setResource( + ResourceInfo::create( + Attributes::create([ + 'telemetry.sdk.name' => 'opentelemetry', + 'telemetry.sdk.language' => 'php', + 'telemetry.sdk.version' => 'dev', + 'instance' => 'test-a' + ]) + ) + ) + ->addEvent('validators.list', Attributes::create(['job' => 'stage.updateTime']), 1505855799433901068) + ->setHasEnded(true); + + $converter = new InstanaSpanConverter('0123456abcdef', '12345'); + $instanaSpan = $converter->convert([$span])[0]; + + $this->assertSame($span->getContext()->getSpanId(), $instanaSpan['s']); + $this->assertSame($span->getContext()->getTraceId(), $instanaSpan['t']); + $this->assertSame('1000000000000000', $instanaSpan['p']); + + $this->assertSame('unknown_service:php', $instanaSpan['data']['otel']['service']['name']); + $this->assertSame($span->getName(), $instanaSpan['n']); + + $this->assertSame(1505855794194, $instanaSpan['ts']); + $this->assertSame(5271, $instanaSpan['d']); + + $this->assertCount(7, $instanaSpan['data']); + + $this->assertSame('Error', $instanaSpan['data']['otel']['status_code']); + $this->assertSame('status_description', $instanaSpan['data']['otel']['error']); + $this->assertSame('authorizationservice.com', $instanaSpan['data']['net']['peer.name']); + $this->assertSame('AuthService', $instanaSpan['data']['peer']['service']); + $this->assertSame('opentelemetry', $instanaSpan['data']['telemetry']['sdk.name']); + $this->assertSame('php', $instanaSpan['data']['telemetry']['sdk.language']); + $this->assertSame('dev', $instanaSpan['data']['telemetry']['sdk.version']); + $this->assertSame('test-a', $instanaSpan['data']['instance']); + $this->assertSame('unknown_service:php', $instanaSpan['data']['otel']['service']['name']); + $this->assertSame('dev-main', $instanaSpan['data']['otel']['service']['version']); + + $this->assertSame('instrumentation_scope_name', $instanaSpan['data']['otel']['scope.name']); + $this->assertSame('instrumentation_scope_version', $instanaSpan['data']['otel']['scope.version']); + $this->assertSame('converter.test', $instanaSpan['data']['service']); + + $this->assertSame('12345', $instanaSpan['f']['e']); + $this->assertSame('0123456abcdef', $instanaSpan['f']['h']); + + $this->assertSame(2, $instanaSpan['k']); + } + + #[DataProvider('spanConverterProvider')] + public function test_should_throw_on_missing_construction(InstanaSpanConverter $converter): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Failed to get agentUuid or agentPid'); + $span = (new SpanData()); + $converter->convert([$span]); + } + + public static function spanConverterProvider(): array + { + return [ + 'default' => [new InstanaSpanConverter()], + 'wo_uuid' => [new InstanaSpanConverter(agentPid: '12345')], + 'wo_pid' => [new InstanaSpanConverter(agentUuid: '0123456abcdef')] + ]; + } + + public function test_should_omit_empty_keys_from_instana_span(): void + { + $span = (new SpanData()); + + $converter = new InstanaSpanConverter('0123456abcdef', '12345'); + $instanaSpan = $converter->convert([$span])[0]; + + $this->assertArrayNotHasKey('p', $instanaSpan); + $this->assertArrayNotHasKey('otel', $instanaSpan['data']); + $this->assertCount(1, $instanaSpan['data']); + } + + #[DataProvider('spanKindProvider')] + public function test_should_convert_otel_span_to_an_instana_span(int $internalSpanKind, ?int $expectedSpanKind): void + { + $span = (new SpanData()) + ->setKind($internalSpanKind); + + $converter = new InstanaSpanConverter('0123456abcdef', '12345'); + $instanaSpan = $converter->convert([$span])[0]; + + $this->assertSame($expectedSpanKind, $instanaSpan['k']); + } + + public static function spanKindProvider(): array + { + return [ + 'server' => [SpanKind::KIND_SERVER, InstanaSpanKind::ENTRY], + 'client' => [SpanKind::KIND_CLIENT, InstanaSpanKind::EXIT], + 'producer' => [SpanKind::KIND_PRODUCER, InstanaSpanKind::EXIT], + 'consumer' => [SpanKind::KIND_CONSUMER, InstanaSpanKind::ENTRY], + 'consumer' => [SpanKind::KIND_INTERNAL, InstanaSpanKind::INTERMEDIATE], + 'default' => [12345, null] // Some unsupported "enum" + ]; + } + + public function test_should_convert_an_event_without_attributes_to_an_empty_event(): void + { + $span = (new SpanData()) + ->addEvent('event.name', Attributes::create([])); + + $converter = new InstanaSpanConverter('0123456abcdef', '12345'); + $instanaSpan = $converter->convert([$span])[0]; + + $this->assertSame('{}', $instanaSpan['data']['events']['event.name']); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod,PossiblyInvalidArrayAccess + */ + public function test_data_are_coerced_correctly_to_strings(): void + { + $listOfStrings = ['string-1', 'string-2']; + $listOfNumbers = [1, 2, 3, 3.1415, 42]; + $listOfBooleans = [true, true, false, true]; + + $span = (new SpanData()) + ->addAttribute('string', 'string') + ->addAttribute('integer-1', 1024) + ->addAttribute('integer-2', 0) + ->addAttribute('float', 1.2345) + ->addAttribute('boolean-1', true) + ->addAttribute('boolean-2', false) + ->addAttribute('list-of-strings', $listOfStrings) + ->addAttribute('list-of-numbers', $listOfNumbers) + ->addAttribute('list-of-booleans', $listOfBooleans); + + $data = (new InstanaSpanConverter('0123456abcdef', '12345'))->convert([$span])[0]['data']; + + // Check that we captured all attributes in data. + $this->assertCount(10, $data); + + $this->assertSame('string', $data['string']); + $this->assertSame(1024, $data['integer-1']); + $this->assertSame(0, $data['integer-2']); + $this->assertSame(1.2345, $data['float']); + $this->assertSame(true, $data['boolean-1']); + $this->assertSame(false, $data['boolean-2']); + + // Lists are recovered and are the same. + $this->assertSame($listOfStrings, $data['list-of-strings']); + $this->assertSame($listOfNumbers, $data['list-of-numbers']); + $this->assertSame($listOfBooleans, $data['list-of-booleans']); + } + + /** + * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/common/mapping-to-non-otlp.md#dropped-attributes-count + */ + #[DataProvider('droppedProvider')] + public function test_displays_non_zero_dropped_counts(int $dropped, bool $expected): void + { + $attributes = $this->createMock(AttributesInterface::class); + $attributes->method('getDroppedAttributesCount')->willReturn($dropped); + $spanData = $this->createMock(SpanDataInterface::class); + $spanData->method('getAttributes')->willReturn($attributes); + $spanData->method('getLinks')->willReturn([]); + $spanData->method('getEvents')->willReturn([]); + $spanData->method('getTotalDroppedEvents')->willReturn($dropped); + $spanData->method('getTotalDroppedLinks')->willReturn($dropped); + + $converter = new InstanaSpanConverter('0123456abcdef', '12345'); + $converted = $converter->convert([$spanData])[0]; + $data = $converted['data']['otel']; + + if ($expected) { + $this->assertArrayHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_EVENTS_COUNT, $data); + $this->assertSame($dropped, $data[InstanaSpanConverter::OTEL_KEY_DROPPED_EVENTS_COUNT]); + $this->assertArrayHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_LINKS_COUNT, $data); + $this->assertSame($dropped, $data[InstanaSpanConverter::OTEL_KEY_DROPPED_LINKS_COUNT]); + $this->assertArrayHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_ATTRIBUTES_COUNT, $data); + $this->assertSame($dropped, $data[InstanaSpanConverter::OTEL_KEY_DROPPED_ATTRIBUTES_COUNT]); + } else { + $this->assertArrayNotHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_EVENTS_COUNT, $data); + $this->assertArrayNotHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_LINKS_COUNT, $data); + $this->assertArrayNotHasKey(InstanaSpanConverter::OTEL_KEY_DROPPED_ATTRIBUTES_COUNT, $data); + } + } + + public static function droppedProvider(): array + { + return [ + 'no dropped' => [0, false], + 'some dropped' => [1, true], + ]; + } + + public function test_events(): void + { + $eventAttributes = $this->createMock(AttributesInterface::class); + $eventAttributes->method('getDroppedAttributesCount')->willReturn(99); + $attributes = [ + 'a_one' => 123, + 'a_two' => 3.14159, + 'a_three' => true, + 'a_four' => false, + ]; + $eventAttributes->method('count')->willReturn(count($attributes)); + $eventAttributes->method('toArray')->willReturn($attributes); + $span = (new SpanData()) + ->setName('events.test') + ->addEvent('event.one', $eventAttributes); + $instanaSpan = (new InstanaSpanConverter('0123456abcdef', '12345'))->convert([$span])[0]; + + $events = $instanaSpan['data']['events']; + + $this->assertTrue(array_key_exists('event.one', $events)); + $this->assertIsString($events['event.one']); + } +} diff --git a/tests/Unit/Contrib/Instana/InstanaSpanExporterFactoryTest.php b/tests/Unit/Contrib/Instana/InstanaSpanExporterFactoryTest.php new file mode 100644 index 000000000..24fdef355 --- /dev/null +++ b/tests/Unit/Contrib/Instana/InstanaSpanExporterFactoryTest.php @@ -0,0 +1,27 @@ +factory = new InstanaSpanExporterFactory(); + } + + public function test_instana_exporter_create(): void + { + $exporter = $this->factory->create(); + $this->assertInstanceOf(SpanExporterInterface::class, $exporter); + } +} diff --git a/tests/Unit/Contrib/Instana/InstanaSpanExporterTest.php b/tests/Unit/Contrib/Instana/InstanaSpanExporterTest.php new file mode 100644 index 000000000..b672dad00 --- /dev/null +++ b/tests/Unit/Contrib/Instana/InstanaSpanExporterTest.php @@ -0,0 +1,31 @@ +