Skip to content

Commit

Permalink
implement mutable ReadWriteLogRecord (#1482)
Browse files Browse the repository at this point in the history
open-telemetry/opentelemetry-specification#3907 implements some new
requirements for logging:
- ReadWriteLogRecord can mutate (eg by processors)
- mutated ReadWriteLogRecord can be seen by later processors

This is a breaking change because LogRecordProcessorInterface onEmit param changes to by-reference
  • Loading branch information
brettmc authored Jan 23, 2025
1 parent 145b87f commit 27dfc42
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 12 deletions.
18 changes: 17 additions & 1 deletion src/SDK/Logs/Exporter/InMemoryExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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(),
];
}
}
2 changes: 1 addition & 1 deletion src/SDK/Logs/LogRecordProcessorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/SDK/Logs/Processor/BatchLogRecordProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/SDK/Logs/Processor/MultiLogRecordProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/SDK/Logs/Processor/NoopLogRecordProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
}

Expand Down
2 changes: 1 addition & 1 deletion src/SDK/Logs/Processor/SimpleLogRecordProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
13 changes: 13 additions & 0 deletions src/SDK/Logs/ReadWriteLogRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
11 changes: 5 additions & 6 deletions src/SDK/Logs/ReadableLogRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +21,7 @@
*/
class ReadableLogRecord extends LogRecord
{
protected AttributesInterface $convertedAttributes;
protected AttributesBuilderInterface $attributesBuilder;
protected SpanContextInterface $spanContext;

public function __construct(
Expand All @@ -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
Expand Down Expand Up @@ -96,6 +95,6 @@ public function getBody()

public function getAttributes(): AttributesInterface
{
return $this->convertedAttributes;
return $this->attributesBuilder->build();
}
}
74 changes: 74 additions & 0 deletions tests/Integration/SDK/Logs/LoggerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Tests\Integration\SDK\Logs;

use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Logs\Exporter\InMemoryExporter;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\Processor\MultiLogRecordProcessor;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogRecordProcessor;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

#[CoversNothing]
class LoggerTest extends TestCase
{
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.40.0/specification/logs/sdk.md#onemit
*/
#[Group('logs-compliance')]
public function test_log_record_mutations_visible_to_later_processors(): void
{
$logRecord = (new LogRecord())
->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
}
}
92 changes: 92 additions & 0 deletions tests/Unit/SDK/Logs/ReadWriteLogRecordTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Unit\SDK\Logs;

use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Logs\LoggerSharedState;
use OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(ReadWriteLogRecord::class)]
class ReadWriteLogRecordTest extends TestCase
{
private ReadWriteLogRecord $record;

public function setUp(): void
{
$limits = (new LogRecordLimitsBuilder())->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());
}
}

0 comments on commit 27dfc42

Please sign in to comment.