Skip to content

Commit 110fdb3

Browse files
committed
improve psr3 log context formatting
string values may contain data that causes export to choke, so: - json encode/decode strings and arrays that may contain strings, using JSON_INVALID_UTF8_SUBSTITUTE - only allow objects that are stringable or json serializable - allow other primitive types
1 parent e485f41 commit 110fdb3

File tree

3 files changed

+146
-1
lines changed

3 files changed

+146
-1
lines changed

src/Instrumentation/Psr3/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"cakephp/log": "*",
3737
"monolog/monolog": "*",
3838
"symfony/console": "*",
39+
"symfony/uid": "*",
3940
"yiisoft/log": "*",
4041
"friendsofphp/php-cs-fixer": "^3",
4142
"phan/phan": "^5.0",

src/Instrumentation/Psr3/src/Formatter.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,52 @@
44

55
namespace OpenTelemetry\Contrib\Instrumentation\Psr3;
66

7+
use JsonSerializable;
8+
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
9+
use Stringable;
710
use Throwable;
811

912
class Formatter
1013
{
14+
15+
use LogsMessagesTrait;
16+
1117
public static function format(array $context): array
1218
{
1319
$formatted = [];
1420
foreach ($context as $key => $value) {
1521
if ($key === 'exception' && $value instanceof Throwable) {
1622
$formatted[$key] = self::formatThrowable($value);
1723
} else {
18-
$formatted[$key] = json_decode(json_encode($value) ?: '');
24+
switch (gettype($value)) {
25+
case 'integer':
26+
case 'double':
27+
case 'boolean':
28+
$formatted[$key] = $value;
29+
30+
break;
31+
case 'string':
32+
case 'array':
33+
// Handle UTF-8 encoding issues
34+
$encoded = json_encode($value, JSON_INVALID_UTF8_SUBSTITUTE);
35+
if ($encoded === false) {
36+
self::logWarning('Failed to encode value: ' . json_last_error_msg());
37+
} else {
38+
$formatted[$key] = json_decode($encoded);
39+
}
40+
41+
break;
42+
case 'object':
43+
if ($value instanceof Stringable) {
44+
$formatted[$key] = (string) $value;
45+
} elseif ($value instanceof JsonSerializable) {
46+
$formatted[$key] = $value->jsonSerialize();
47+
}
48+
49+
break;
50+
default:
51+
//do nothing
52+
}
1953
}
2054
}
2155

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
--TEST--
2+
Test generating otel LogRecord with complex message context
3+
--FILE--
4+
5+
<?php
6+
use OpenTelemetry\API\Globals;
7+
use Psr\Log\LoggerInterface;
8+
use Psr\Log\LoggerTrait;
9+
10+
putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
11+
putenv('OTEL_LOGS_EXPORTER=console');
12+
putenv('OTEL_TRACES_EXPORTER=none');
13+
putenv('OTEL_METRICS_EXPORTER=none');
14+
putenv('OTEL_PHP_DETECTORS=none');
15+
putenv('OTEL_PHP_PSR3_MODE=export');
16+
17+
require dirname(__DIR__, 2) . '/vendor/autoload.php';
18+
19+
$logger = new class implements LoggerInterface{
20+
use LoggerTrait;
21+
public function log($level, string|\Stringable $message, array $context = []): void {}
22+
};
23+
24+
$span = Globals::tracerProvider()->getTracer('demo')->spanBuilder('root')->startSpan();
25+
$scope = $span->activate();
26+
27+
$context = [
28+
's' => 'string',
29+
'i' => 1234,
30+
'l' => 3.14159,
31+
't' => true,
32+
'f' => false,
33+
'stringable' => new class() implements \Stringable {
34+
public function __toString(): string
35+
{
36+
return 'some_string';
37+
}
38+
},
39+
'j' => new class() implements JsonSerializable {
40+
public function jsonSerialize(): array
41+
{
42+
return ['key' => 'value'];
43+
}
44+
},
45+
'array' => ['a', 'b', 'c'],
46+
'exception' => new \Exception('my_exception'),
47+
'bin' => \Symfony\Component\Uid\Uuid::v4()->toBinary(),
48+
];
49+
50+
51+
$logger->info('test message', $context);
52+
53+
$scope->detach();
54+
$span->end();
55+
?>
56+
57+
--EXPECTF--
58+
{
59+
"resource": {
60+
"attributes": [],
61+
"dropped_attributes_count": 0
62+
},
63+
"scopes": [
64+
{
65+
"name": "io.opentelemetry.contrib.php.psr3",
66+
"version": null,
67+
"attributes": [],
68+
"dropped_attributes_count": 0,
69+
"schema_url": "https:\/\/opentelemetry.io\/schemas\/%s",
70+
"logs": [
71+
{
72+
"timestamp": null,
73+
"observed_timestamp": %d,
74+
"severity_number": %d,
75+
"severity_text": null,
76+
"body": "test message",
77+
"trace_id": "%s",
78+
"span_id": "%s",
79+
"trace_flags": 1,
80+
"attributes": {
81+
"s": "string",
82+
"i": 1234,
83+
"l": 3.14159,
84+
"t": true,
85+
"f": false,
86+
"stringable": "some_string",
87+
"j": {
88+
"key": "value"
89+
},
90+
"array": [
91+
"a",
92+
"b",
93+
"c"
94+
],
95+
"exception": {
96+
"message": "my_exception",
97+
"code": 0,
98+
"file": "Standard input code",
99+
"line": %d,
100+
"trace": [],
101+
"previous": []
102+
},
103+
"bin": "%s"
104+
},
105+
"dropped_attributes_count": 0
106+
}
107+
]
108+
}
109+
]
110+
}

0 commit comments

Comments
 (0)