diff --git a/src/ExtendedLoggerEntry.php b/src/ExtendedLoggerEntry.php index ddcdd9727d38bc5298275c0fe5d8e578cc8f500f..25dfea4478b99a4f5497b1a4e39eceee5c4f8023 100644 --- a/src/ExtendedLoggerEntry.php +++ b/src/ExtendedLoggerEntry.php @@ -3,7 +3,7 @@ namespace Drupal\extended_logger; /** - * Redirects logging messages to syslog or stdout. + * The Extended Logger entry object, containing all data that will be logged. * * No declared properties listed, because the data can be structured in a free * form, and will be converted to JSON. @@ -11,5 +11,67 @@ namespace Drupal\extended_logger; * @see Drupal\extended_logger\Form\SettingsForm::LOGGER_FIELDS for the list of * common possible values. */ -class ExtendedLoggerEntry extends \stdClass { +class ExtendedLoggerEntry implements ExtendedLoggerEntryInterface { + + /** + * A storage of the log entry data. + * + * @var array + */ + protected array $data; + + /** + * {@inheritdoc} + */ + public function __construct(array $data = NULL) { + $this->data = is_array($data) + ? $data + : []; + } + + /** + * {@inheritdoc} + */ + public function set(string $key, $value): ExtendedLoggerEntry { + $this->data[$key] = $value; + return $this; + } + + /** + * {@inheritdoc} + */ + public function get(string $key): mixed { + return $this->data[$key] ?? NULL; + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): ExtendedLoggerEntry { + unset($this->data[$key]); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setData(array $data): ExtendedLoggerEntry { + $this->data = $data; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getData(): array { + return $this->data; + } + + /** + * {@inheritdoc} + */ + public function __toString(): string { + return json_encode($this->data); + } + } diff --git a/src/ExtendedLoggerEntryInterface.php b/src/ExtendedLoggerEntryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..5ec214bbb662f4b433ec8a4b2432f18ac8e61c74 --- /dev/null +++ b/src/ExtendedLoggerEntryInterface.php @@ -0,0 +1,79 @@ +<?php + +namespace Drupal\extended_logger; + +/** + * The interface for an Extended Logger entry class. + * + * The class contains all the data that will be logged. + * + * @see Drupal\extended_logger\Logger\ExtendedLogger::LOGGER_FIELDS for the list + * of common possible values. + */ +interface ExtendedLoggerEntryInterface { + + /** + * The ExtendedLoggerEntry constructor. + * + * @param array $data + * (optional) An array with initial data. + */ + public function __construct(array $data = NULL); + + /** + * Sets a value to the log entry by a key. + * + * @param string $key + * A key. + * @param mixed $value + * A value. + * + * @return \Drupal\extended_logger\ExtendedLoggerEntry + * The ExtendedLoggerEntry object. + */ + public function set(string $key, $value): ExtendedLoggerEntry; + + /** + * Gets a value from the log entry by a key. + * + * @param string $key + * A key. + * + * @return mixed + * The value by the key. + */ + public function get(string $key): mixed; + + /** + * Deletes a value from the log entry by a key. + * + * @param string $key + * A key. + * + * @return \Drupal\extended_logger\ExtendedLoggerEntry + * The ExtendedLoggerEntry object. + */ + public function delete(string $key): ExtendedLoggerEntry; + + /** + * Sets the whole data of the log entry. + * + * @param array $data + * The array with data. + * + * @return \Drupal\extended_logger\ExtendedLoggerEntry + * The ExtendedLoggerEntry object. + */ + public function setData(array $data): ExtendedLoggerEntry; + + /** + * Gets the whole log entry data. + */ + public function getData(): mixed; + + /** + * Converts the log data to the string represenation. + */ + public function __toString(): string; + +} diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 1cdf4917b2ea12750918b3045f7941110f635045..a81c6340977bd371dfc676b23eb3b9f9448c92c6 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -2,7 +2,6 @@ namespace Drupal\extended_logger\Form; -use Drupal\Core\Config\Schema\Undefined; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; @@ -191,12 +190,11 @@ class SettingsForm extends ConfigFormBase { * Gets the label for a setting from typed settings object. */ private function getSettingLabel(string $key, ?string $fallback = NULL): string { - $setting = $this->settingsTyped->get($key); - if ($setting instanceof Undefined) { - $label = $fallback ?: "[$key]"; + try { + $label = $this->settingsTyped->get($key)->getDataDefinition()->getLabel(); } - else { - $label = $setting->getDataDefinition()->getLabel(); + catch (\InvalidArgumentException $e) { + $label = $fallback ?: "[$key]"; } return $label; } diff --git a/src/Logger/ExtendedLogger.php b/src/Logger/ExtendedLogger.php index c6d2bb45d92de8e77ccfc23cad5939429b5607c4..01da386f943d4f44b1ffb19050f40c2058abe5dd 100644 --- a/src/Logger/ExtendedLogger.php +++ b/src/Logger/ExtendedLogger.php @@ -9,6 +9,9 @@ use Drupal\Core\Logger\RfcLoggerTrait; use Drupal\Core\Logger\RfcLogLevel; use Drupal\extended_logger\Event\ExtendedLoggerLogEvent; use Drupal\extended_logger\ExtendedLoggerEntry; +use OpenTelemetry\API\Trace\SpanContextInterface; +use OpenTelemetry\API\Trace\SpanInterface; +use OpenTelemetry\SDK\Trace\Span; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -32,6 +35,7 @@ class ExtendedLogger implements LoggerInterface { const CONFIG_KEY = 'extended_logger.settings'; const LOGGER_FIELDS = [ + 'time' => 'The timestamp as a string implementation in the "c" format.', 'timestamp' => 'The log entry timestamp.', 'timestamp_float' => 'The log entry timestamp in milliseconds.', 'message' => 'The rendered log message with replaced placeholders.', @@ -114,44 +118,49 @@ class ExtendedLogger implements LoggerInterface { switch ($label) { case 'message': $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context); - $entry->$label = empty($message_placeholders) ? $message : strtr($message, $message_placeholders); + $entry->set($label, empty($message_placeholders) ? $message : strtr($message, $message_placeholders)); break; case 'message_raw': - $entry->$label = $message; + $entry->set($label, $message); break; case 'base_url': - $entry->$label = $base_url; + $entry->set($label, $base_url); break; case 'timestamp_float': - $entry->$label = microtime(TRUE); + $entry->set($label, microtime(TRUE)); + break; + + case 'time': + $entry->set($label, date('c', $context['timestamp'])); break; case 'request_time': $request ??= $this->requestStack->getCurrentRequest(); - $entry->$label = $request->server->get('REQUEST_TIME'); + $entry->set($label, $request->server->get('REQUEST_TIME')); break; case 'request_time_float': $request ??= $this->requestStack->getCurrentRequest(); - $entry->$label = $request->server->get('REQUEST_TIME_FLOAT'); + $entry->set($label, $request->server->get('REQUEST_TIME_FLOAT')); break; case 'severity': - $entry->$label = $level; + $entry->set($label, $level); break; case 'level': - $entry->$label = $this->getRfcLogLevelAsString($level); + $entry->set($label, $this->getRfcLogLevelAsString($level)); break; // A special label "metadata" to pass any free form data. case 'metadata': if (isset($context[$label])) { - $entry->$label = $context[$label]; + $entry->set($label, $context[$label]); } + break; // Default context keys from Drupal Core. case 'timestamp': @@ -163,15 +172,29 @@ class ExtendedLogger implements LoggerInterface { case 'link': default: if (isset($context[$label])) { - $entry->$label = $context[$label]; + $entry->set($label, $context[$label]); } } } + foreach ($this->config->get('fields_custom') ?? [] as $field) { if (isset($context[$field])) { - $entry->$field = $context[$field]; + $entry->set($field, $context[$field]); } } + + // If we have an OpenTelemetry span, add the trace id to the log entry. + if (class_exists(Span::class)) { + $span = Span::getCurrent(); + if ($span instanceof SpanInterface) { + $spanContext = $span->getContext(); + if ($spanContext instanceof SpanContextInterface) { + $traceId = $spanContext->getTraceId(); + $entry->set('trace_id', $traceId); + } + } + } + $event = new ExtendedLoggerLogEvent($entry, $level, $message, $context); $this->eventDispatcher->dispatch($event); @@ -181,30 +204,29 @@ class ExtendedLogger implements LoggerInterface { /** * Persists a log entry to the log target. * - * @param array $entry + * @param \Drupal\extended_logger\ExtendedLoggerEntry $entry * A log entry array. * @param int $level * The log entry level. */ protected function persist(ExtendedLoggerEntry $entry, int $level): void { - $entryString = json_encode($entry); $target = $this->config->get('target') ?? 'syslog'; switch ($target) { case 'syslog': if (!$this->getSyslogConnection()) { throw new \Exception("Can't open the connection to syslog"); } - syslog($level, $entryString); + syslog($level, $entry->__toString()); break; case 'output': - file_put_contents('php://' . $this->config->get('target_output_stream') ?? 'stdout', $entryString . "\n"); + file_put_contents('php://' . $this->config->get('target_output_stream') ?? 'stdout', $entry->__toString() . "\n"); break; case 'file': $file = $this->config->get('target_file_path'); if (!empty($file)) { - file_put_contents($file, $entryString . "\n", FILE_APPEND); + file_put_contents($file, $entry->__toString() . "\n", FILE_APPEND); } break; diff --git a/tests/src/Unit/ExtendedLoggerEntryTest.php b/tests/src/Unit/ExtendedLoggerEntryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..df8f43543ae9ac533eb85e962359d9d66349f0e3 --- /dev/null +++ b/tests/src/Unit/ExtendedLoggerEntryTest.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\Tests\extended_logger\Unit; + +use Drupal\extended_logger\ExtendedLoggerEntry; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\extended_logger\ExtendedLoggerEntry + * @group extended_logger + */ +class ExtendedLoggerEntryTest extends UnitTestCase { + + /** + * @covers ::__construct + * @covers ::get + * @covers ::set + * @covers ::delete + * @covers ::getData + * @covers ::setData + * @covers ::__toString + */ + public function testGeneral() { + $entry0 = new ExtendedLoggerEntry(); + $this->assertEquals([], $entry0->getData()); + + $data1 = ['foo' => 'bar', 'baz' => 'qix']; + $entry1 = new ExtendedLoggerEntry($data1); + $this->assertEquals($data1, $entry1->getData()); + $this->assertEquals($data1['foo'], $entry1->get('foo')); + + $data2 = $data1 + ['fred' => 'thud']; + $entry1->set('fred', $data2['fred']); + $this->assertEquals($data2, $entry1->getData()); + $this->assertEquals($data2['fred'], $entry1->get('fred')); + $this->assertEquals(json_encode($data2), $entry1->__toString()); + + $entry1->delete('fred'); + $this->assertEquals($data1, $entry1->getData()); + + $entry1->setData($data2); + $this->assertEquals($data2['fred'], $entry1->get('fred')); + } + +} diff --git a/tests/src/Unit/ExtendedLoggerTest.php b/tests/src/Unit/ExtendedLoggerTest.php index ccd6867f549e92de9443c0baf4dcd20632908fa1..035d4eace0917cef860852bc8e91e5b09945954f 100644 --- a/tests/src/Unit/ExtendedLoggerTest.php +++ b/tests/src/Unit/ExtendedLoggerTest.php @@ -76,9 +76,12 @@ class ExtendedLoggerTest extends UnitTestCase { ); $logger->method('persist')->willReturnCallback( function (ExtendedLoggerEntry $entry, int $level) use ($logLevel, $resultEntry) { - $this->assertIsFloat($entry->timestamp_float); - unset($entry->timestamp_float); - $this->assertEquals($resultEntry, $entry); + $this->assertIsFloat($entry->get('timestamp_float')); + $entry->delete('timestamp_float'); + if ($entry->get('trace_id')) { + $entry->delete('trace_id'); + } + $this->assertEquals(json_encode($resultEntry), $entry->__toString()); $this->assertEquals($logLevel, $level); }); @@ -89,8 +92,10 @@ class ExtendedLoggerTest extends UnitTestCase { * @covers ::persist */ public function testPersist() { - $entry = new ExtendedLoggerEntry(); - $entry->foo = '$bar'; + $entryData = [ + 'foo' => 'bar', + ]; + $entry = new ExtendedLoggerEntry($entryData); $level = RfcLogLevel::EMERGENCY; $configDefault = Yaml::parseFile(TestHelpers::getModuleFilePath('config/install/extended_logger.settings.yml')); @@ -105,7 +110,7 @@ class ExtendedLoggerTest extends UnitTestCase { TestHelpers::callPrivateMethod($logger, 'persist', [$entry, $level]); $this->assertEquals($config['target_file_path'], $calls[0][0]); - $this->assertEquals(json_encode($entry) . "\n", $calls[0][1]); + $this->assertEquals(json_encode($entryData) . "\n", $calls[0][1]); // Test writing to the stderr. $config = [ @@ -117,7 +122,7 @@ class ExtendedLoggerTest extends UnitTestCase { $logger = TestHelpers::initService('extended_logger.logger'); TestHelpers::callPrivateMethod($logger, 'persist', [$entry, $level]); $this->assertEquals('php://stderr', $calls[0][0]); - $this->assertEquals(json_encode($entry) . "\n", $calls[0][1]); + $this->assertEquals(json_encode($entry->getData()) . "\n", $calls[0][1]); // Test writing to syslog. $config = [ @@ -135,7 +140,7 @@ class ExtendedLoggerTest extends UnitTestCase { $this->assertEquals($config['target_syslog_identity'], $openlogCalls[0][0]); $this->assertEquals($config['target_syslog_facility'], $openlogCalls[0][2]); $this->assertEquals($level, $syslogCalls[0][0]); - $this->assertEquals(json_encode($entry), $syslogCalls[0][1]); + $this->assertEquals($entry->__toString(), $syslogCalls[0][1]); } }