Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Instrumentation/Psr3/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"cakephp/log": "*",
"monolog/monolog": "*",
"symfony/console": "*",
"symfony/uid": "*",
"yiisoft/log": "*",
"friendsofphp/php-cs-fixer": "^3",
"phan/phan": "^5.0",
Expand Down
36 changes: 35 additions & 1 deletion src/Instrumentation/Psr3/src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,52 @@

namespace OpenTelemetry\Contrib\Instrumentation\Psr3;

use JsonSerializable;
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use Stringable;
use Throwable;

class Formatter
{

use LogsMessagesTrait;

public static function format(array $context): array
{
$formatted = [];
foreach ($context as $key => $value) {
if ($key === 'exception' && $value instanceof Throwable) {
$formatted[$key] = self::formatThrowable($value);
} else {
$formatted[$key] = json_decode(json_encode($value) ?: '');
switch (gettype($value)) {
case 'integer':
case 'double':
case 'boolean':
$formatted[$key] = $value;

break;
case 'string':
case 'array':
// Handle UTF-8 encoding issues
$encoded = json_encode($value, JSON_INVALID_UTF8_SUBSTITUTE);
if ($encoded === false) {
self::logWarning('Failed to encode value: ' . json_last_error_msg());
} else {
$formatted[$key] = json_decode($encoded);
}

break;
case 'object':
if ($value instanceof Stringable) {
$formatted[$key] = (string) $value;
} elseif ($value instanceof JsonSerializable) {
$formatted[$key] = $value->jsonSerialize();
}

break;
default:
//do nothing
}
}
}

Expand Down
53 changes: 52 additions & 1 deletion src/Instrumentation/Psr3/tests/Unit/FormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,74 @@

namespace OpenTelemetry\Contrib\Psr3\tests\Unit;

use Exception;
use JsonSerializable;
use OpenTelemetry\API\Behavior\Internal\Logging;
use OpenTelemetry\API\Behavior\Internal\LogWriter\LogWriterInterface;
use OpenTelemetry\Contrib\Instrumentation\Psr3\Formatter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Stringable;

class FormatterTest extends TestCase
{
/** @var LogWriterInterface&MockObject */
private LogWriterInterface $logWriter;

public function setUp(): void
{
$this->logWriter = $this->createMock(LogWriterInterface::class);
Logging::setLogWriter($this->logWriter);
}

public function tearDown(): void
{
Logging::reset();
}

public function test_format(): void
{
$context = [
0 => 'zero',
'foo' => 'bar',
'exception' => new \Exception('foo', 500, new \RuntimeException('bar')),
'exception' => new Exception('foo', 500, new \RuntimeException('bar')),
'j' => new class() implements JsonSerializable {
public function jsonSerialize(): array
{
return ['foo' => 'bar'];
}
},
's' => new class() implements Stringable {
public function __toString(): string
{
return 'string_value';
}
},
'b' => true,
];
$formatted = Formatter::format($context);
$this->assertSame('bar', $formatted['foo']);
$this->assertArrayHasKey('exception', $formatted);
$this->assertSame('foo', $formatted['exception']['message']);
$this->assertSame(500, $formatted['exception']['code']);
$this->assertSame('bar', $formatted['exception']['previous']['message']);
$this->assertSame('string_value', $formatted['s']);
$this->assertSame(['foo' => 'bar'], $formatted['j']);
$this->assertTrue($formatted['b']);
}

public function test_invalid_input_logs_warning(): void
{
$this->logWriter->expects($this->once())->method('write')->with(
$this->equalTo('warning'),
$this->stringContains('Failed to encode value'),
);
$context = [
'good' => 'foo',
'bad' => [fopen('php://memory', 'r+')], //resource cannot be encoded
];
$formatted = Formatter::format($context);
$this->assertSame('foo', $formatted['good']);
$this->assertArrayNotHasKey('bad', $formatted);
}
}
110 changes: 110 additions & 0 deletions src/Instrumentation/Psr3/tests/phpt/export_context.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
--TEST--
Test generating otel LogRecord with complex message context
--FILE--

<?php
use OpenTelemetry\API\Globals;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;

putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
putenv('OTEL_LOGS_EXPORTER=console');
putenv('OTEL_TRACES_EXPORTER=none');
putenv('OTEL_METRICS_EXPORTER=none');
putenv('OTEL_PHP_DETECTORS=none');
putenv('OTEL_PHP_PSR3_MODE=export');

require dirname(__DIR__, 2) . '/vendor/autoload.php';

$logger = new class implements LoggerInterface{
use LoggerTrait;
public function log($level, string|\Stringable $message, array $context = []): void {}
};

$span = Globals::tracerProvider()->getTracer('demo')->spanBuilder('root')->startSpan();
$scope = $span->activate();

$context = [
's' => 'string',
'i' => 1234,
'l' => 3.14159,
't' => true,
'f' => false,
'stringable' => new class() implements \Stringable {
public function __toString(): string
{
return 'some_string';
}
},
'j' => new class() implements JsonSerializable {
public function jsonSerialize(): array
{
return ['key' => 'value'];
}
},
'array' => ['a', 'b', 'c'],
'exception' => new \Exception('my_exception'),
'bin' => \Symfony\Component\Uid\Uuid::v4()->toBinary(),
];


$logger->info('test message', $context);

$scope->detach();
$span->end();
?>

--EXPECTF--
{
"resource": {
"attributes": [],
"dropped_attributes_count": 0
},
"scopes": [
{
"name": "io.opentelemetry.contrib.php.psr3",
"version": null,
"attributes": [],
"dropped_attributes_count": 0,
"schema_url": "https:\/\/opentelemetry.io\/schemas\/%s",
"logs": [
{
"timestamp": null,
"observed_timestamp": %d,
"severity_number": %d,
"severity_text": null,
"body": "test message",
"trace_id": "%s",
"span_id": "%s",
"trace_flags": 1,
"attributes": {
"s": "string",
"i": 1234,
"l": 3.14159,
"t": true,
"f": false,
"stringable": "some_string",
"j": {
"key": "value"
},
"array": [
"a",
"b",
"c"
],
"exception": {
"message": "my_exception",
"code": 0,
"file": "Standard input code",
"line": %d,
"trace": [],
"previous": []
},
"bin": "%s"
},
"dropped_attributes_count": 0
}
]
}
]
}