diff --git a/src/SDK/Logs/Exporter/InMemoryExporter.php b/src/SDK/Logs/Exporter/InMemoryExporter.php index 3ecaecd3d..52615d2ba 100644 --- a/src/SDK/Logs/Exporter/InMemoryExporter.php +++ b/src/SDK/Logs/Exporter/InMemoryExporter.php @@ -9,6 +9,7 @@ use OpenTelemetry\SDK\Common\Future\CompletedFuture; use OpenTelemetry\SDK\Common\Future\FutureInterface; use OpenTelemetry\SDK\Logs\LogRecordExporterInterface; +use OpenTelemetry\SDK\Logs\ReadableLogRecord; class InMemoryExporter implements LogRecordExporterInterface { @@ -22,7 +23,7 @@ public function __construct(private readonly ArrayObject $storage = new ArrayObj public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface { foreach ($batch as $record) { - $this->storage->append($record); + $this->storage->append($this->convert($record)); } return new CompletedFuture(true); @@ -42,4 +43,19 @@ public function getStorage(): ArrayObject { return $this->storage; } + + private function convert(ReadableLogRecord $record): array + { + return [ + 'timestamp' => $record->getTimestamp(), + 'observed_timestamp' => $record->getObservedTimestamp(), + 'severity_number' => $record->getSeverityNumber(), + 'severity_text' => $record->getSeverityText(), + 'body' => $record->getBody(), + 'attributes' => $record->getAttributes()->toArray(), + 'trace_id' => $record->getSpanContext()?->getTraceId(), + 'span_id' => $record->getSpanContext()?->getSpanId(), + 'trace_flags' => $record->getSpanContext()?->getTraceFlags(), + ]; + } } diff --git a/src/SDK/Logs/LogRecordProcessorInterface.php b/src/SDK/Logs/LogRecordProcessorInterface.php index 1977d48fd..ba1519a3a 100644 --- a/src/SDK/Logs/LogRecordProcessorInterface.php +++ b/src/SDK/Logs/LogRecordProcessorInterface.php @@ -9,7 +9,7 @@ interface LogRecordProcessorInterface { - public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void; + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void; public function shutdown(?CancellationInterface $cancellation = null): bool; public function forceFlush(?CancellationInterface $cancellation = null): bool; } diff --git a/src/SDK/Logs/Processor/BatchLogRecordProcessor.php b/src/SDK/Logs/Processor/BatchLogRecordProcessor.php index 8de2d0f30..6fcd23e39 100644 --- a/src/SDK/Logs/Processor/BatchLogRecordProcessor.php +++ b/src/SDK/Logs/Processor/BatchLogRecordProcessor.php @@ -134,7 +134,7 @@ public function __construct( }); } - public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void { if ($this->closed) { return; diff --git a/src/SDK/Logs/Processor/MultiLogRecordProcessor.php b/src/SDK/Logs/Processor/MultiLogRecordProcessor.php index 6a5791f19..4bb6c5433 100644 --- a/src/SDK/Logs/Processor/MultiLogRecordProcessor.php +++ b/src/SDK/Logs/Processor/MultiLogRecordProcessor.php @@ -22,7 +22,7 @@ public function __construct(array $processors) } } - public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void { foreach ($this->processors as $processor) { $processor->onEmit($record, $context); diff --git a/src/SDK/Logs/Processor/NoopLogRecordProcessor.php b/src/SDK/Logs/Processor/NoopLogRecordProcessor.php index 7028052e1..3fabcfd28 100644 --- a/src/SDK/Logs/Processor/NoopLogRecordProcessor.php +++ b/src/SDK/Logs/Processor/NoopLogRecordProcessor.php @@ -21,7 +21,7 @@ public static function getInstance(): self /** * @codeCoverageIgnore */ - public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void { } diff --git a/src/SDK/Logs/Processor/SimpleLogRecordProcessor.php b/src/SDK/Logs/Processor/SimpleLogRecordProcessor.php index 264a450ae..6df8a3ff8 100644 --- a/src/SDK/Logs/Processor/SimpleLogRecordProcessor.php +++ b/src/SDK/Logs/Processor/SimpleLogRecordProcessor.php @@ -19,7 +19,7 @@ public function __construct(private readonly LogRecordExporterInterface $exporte /** * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#onemit */ - public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void { $this->exporter->export([$record]); } diff --git a/src/SDK/Logs/ReadWriteLogRecord.php b/src/SDK/Logs/ReadWriteLogRecord.php index 9bb4b1564..74e1fb4e0 100644 --- a/src/SDK/Logs/ReadWriteLogRecord.php +++ b/src/SDK/Logs/ReadWriteLogRecord.php @@ -6,4 +6,17 @@ class ReadWriteLogRecord extends ReadableLogRecord { + public function setAttribute(string $name, mixed $value): self + { + $this->attributesBuilder->offsetSet($name, $value); + + return $this; + } + + public function removeAttribute(string $key): self + { + $this->attributesBuilder->offsetUnset($key); + + return $this; + } } diff --git a/src/SDK/Logs/ReadableLogRecord.php b/src/SDK/Logs/ReadableLogRecord.php index f1d09e539..e11839fb7 100644 --- a/src/SDK/Logs/ReadableLogRecord.php +++ b/src/SDK/Logs/ReadableLogRecord.php @@ -9,6 +9,7 @@ use OpenTelemetry\API\Trace\SpanContextInterface; use OpenTelemetry\Context\Context; use OpenTelemetry\Context\ContextInterface; +use OpenTelemetry\SDK\Common\Attribute\AttributesBuilderInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Common\Attribute\LogRecordAttributeValidator; use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; @@ -20,7 +21,7 @@ */ class ReadableLogRecord extends LogRecord { - protected AttributesInterface $convertedAttributes; + protected AttributesBuilderInterface $attributesBuilder; protected SpanContextInterface $spanContext; public function __construct( @@ -38,12 +39,10 @@ public function __construct( $this->severityNumber = $logRecord->severityNumber; $this->severityText = $logRecord->severityText; - //convert attributes now so that excess data is not sent to processors - $this->convertedAttributes = $this->loggerSharedState + $this->attributesBuilder = $this->loggerSharedState ->getLogRecordLimits() ->getAttributeFactory() - ->builder($logRecord->attributes, new LogRecordAttributeValidator()) - ->build(); + ->builder($logRecord->attributes, new LogRecordAttributeValidator()); } public function getInstrumentationScope(): InstrumentationScopeInterface @@ -96,6 +95,6 @@ public function getBody() public function getAttributes(): AttributesInterface { - return $this->convertedAttributes; + return $this->attributesBuilder->build(); } } diff --git a/tests/Integration/SDK/Logs/LoggerTest.php b/tests/Integration/SDK/Logs/LoggerTest.php new file mode 100644 index 000000000..6843dfb8f --- /dev/null +++ b/tests/Integration/SDK/Logs/LoggerTest.php @@ -0,0 +1,74 @@ +setAttributes(['foo' => 'bar']); + $storage = new \ArrayObject(); + $exporter = new InMemoryExporter($storage); + $mutator = new class($exporter) implements LogRecordProcessorInterface { + public function __construct(private readonly InMemoryExporter $exporter) + { + } + + public function onEmit(ReadWriteLogRecord &$record, ?ContextInterface $context = null): void + { + $record->setAttributes(['baz' => 'bat']); + $this->exporter->export([$record]); + } + + public function shutdown(?CancellationInterface $cancellation = null): bool + { + return true; + } + + public function forceFlush(?CancellationInterface $cancellation = null): bool + { + return true; + } + }; + $multi = new MultiLogRecordProcessor([ + new SimpleLogRecordProcessor($exporter), + $mutator, + new SimpleLogRecordProcessor($exporter), + ]); + $logger = LoggerProvider::builder()->addLogRecordProcessor($multi)->build()->getLogger('test'); + + $this->assertCount(0, $storage); + $logger->emit($logRecord); + $this->assertCount(3, $storage); + + $first = $storage[0]; //@var array $first + $this->assertSame(['foo' => 'bar'], $first['attributes'], 'original attributes'); //@phpstan-ignore-line + + $second = $storage[1]; //@var array $second + $this->assertSame(['foo' => 'bar', 'baz' => 'bat'], $second['attributes'], 'mutated attributes'); //@phpstan-ignore-line + + $third = $storage[2]; //@var array $third + $this->assertSame(['foo' => 'bar', 'baz' => 'bat'], $third['attributes'], 'attributes after mutation by second processor'); //@phpstan-ignore-line + } +} diff --git a/tests/Unit/SDK/Logs/ReadWriteLogRecordTest.php b/tests/Unit/SDK/Logs/ReadWriteLogRecordTest.php new file mode 100644 index 000000000..e6179b16b --- /dev/null +++ b/tests/Unit/SDK/Logs/ReadWriteLogRecordTest.php @@ -0,0 +1,92 @@ +setAttributeCountLimit(10)->build(); + $loggerSharedState = new LoggerSharedState( + ResourceInfoFactory::emptyResource(), + $limits, + $this->createMock(LogRecordProcessorInterface::class) + ); + $record = (new LogRecord()) + ->setTimestamp(1) + ->setObservedTimestamp(2) + ->setSeverityText('severity') + ->setSeverityNumber(3) + ->setBody('body') + ->setAttributes(['key' => 'value']); + + $this->record = new ReadWriteLogRecord( + $this->createMock(InstrumentationScopeInterface::class), + $loggerSharedState, + $record + ); + } + + public function test_modify_timestamp(): void + { + $this->record->setTimestamp(4); + $this->assertEquals(4, $this->record->getTimestamp()); + } + + public function test_set_observed_timestamp(): void + { + $this->record->setObservedTimestamp(5); + $this->assertEquals(5, $this->record->getObservedTimestamp()); + } + + public function test_set_severity_text(): void + { + $this->record->setSeverityText('severity2'); + $this->assertEquals('severity2', $this->record->getSeverityText()); + } + + public function test_set_severity_number(): void + { + $this->record->setSeverityNumber(6); + $this->assertEquals(6, $this->record->getSeverityNumber()); + } + + public function test_set_body(): void + { + $this->record->setBody('body2'); + $this->assertEquals('body2', $this->record->getBody()); + } + + public function test_add_attribute(): void + { + $this->record->setAttribute('key2', 'value2'); + $this->assertEquals(['key' => 'value', 'key2' => 'value2'], $this->record->getAttributes()->toArray()); + } + + public function test_remove_attribute(): void + { + $this->record->removeAttribute('key'); + $this->assertEquals([], $this->record->getAttributes()->toArray()); + } + + public function test_modify_attribute(): void + { + $this->record->setAttribute('key', 'updated'); + $this->assertEquals(['key' => 'updated'], $this->record->getAttributes()->toArray()); + } +}