From c7ec64894c9f3fa867fec409655a6c19155f0387 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Fri, 21 Mar 2025 18:18:46 +0100 Subject: [PATCH 01/45] Add Sentry logs --- src/Event.php | 22 ++++++ src/EventType.php | 6 ++ src/Logs/LogLevel.php | 42 +++++++++++ src/Logs/Logs.php | 44 +++++++++++ src/Logs/LogsAggregator.php | 96 ++++++++++++++++++++++++ src/Serializer/EnvelopItems/LogsItem.php | 31 ++++++++ src/Serializer/PayloadSerializer.php | 4 + src/functions.php | 6 ++ 8 files changed, 251 insertions(+) create mode 100644 src/Logs/LogLevel.php create mode 100644 src/Logs/Logs.php create mode 100644 src/Logs/LogsAggregator.php create mode 100644 src/Serializer/EnvelopItems/LogsItem.php diff --git a/src/Event.php b/src/Event.php index 0148069ac..67f5f3407 100644 --- a/src/Event.php +++ b/src/Event.php @@ -61,6 +61,11 @@ final class Event */ private $checkIn; + /** + * @var array|null + */ + private $logs; + /** * @var string|null The name of the server (e.g. the host name) */ @@ -216,6 +221,11 @@ public static function createCheckIn(?EventId $eventId = null): self return new self($eventId, EventType::checkIn()); } + public static function createLogs(?EventId $eventId = null): self + { + return new self($eventId, EventType::logs()); + } + /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ @@ -368,6 +378,18 @@ public function setCheckIn(?CheckIn $checkIn): self return $this; } + public function getLogs(): array + { + return $this->logs; + } + + public function setLogs(array $logs): self + { + $this->logs = $logs; + + return $this; + } + /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ diff --git a/src/EventType.php b/src/EventType.php index b8ffdef94..3c2d13fb3 100644 --- a/src/EventType.php +++ b/src/EventType.php @@ -42,6 +42,11 @@ public static function checkIn(): self return self::getInstance('check_in'); } + public static function logs(): self + { + return self::getInstance('log'); + } + /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ @@ -61,6 +66,7 @@ public static function cases(): array self::event(), self::transaction(), self::checkIn(), + self::logs(), self::metrics(), ]; } diff --git a/src/Logs/LogLevel.php b/src/Logs/LogLevel.php new file mode 100644 index 000000000..6e61f2849 --- /dev/null +++ b/src/Logs/LogLevel.php @@ -0,0 +1,42 @@ + A list of cached enum instances + */ + private static $instances = []; + + private function __construct(string $value) + { + $this->value = $value; + } + + public static function info(): self + { + return self::getInstance('info'); + } + + public function __toString(): string + { + return $this->value; + } + + private static function getInstance(string $value): self + { + if (!isset(self::$instances[$value])) { + self::$instances[$value] = new self($value); + } + + return self::$instances[$value]; + } +} diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php new file mode 100644 index 000000000..b639d64bc --- /dev/null +++ b/src/Logs/Logs.php @@ -0,0 +1,44 @@ +aggregator = new LogsAggregator(); + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function info(string $message): void + { + $this->aggregator->add(LogLevel::info(), $message); + } + + public function flush(): ?EventId + { + return $this->aggregator->flush(); + } +} diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php new file mode 100644 index 000000000..d5e6ee202 --- /dev/null +++ b/src/Logs/LogsAggregator.php @@ -0,0 +1,96 @@ +getSpan(); + if ($span !== null) { + $traceId = $span->getTraceId(); + } + + $this->logs[] = [ + 'timestamp' => $timestamp, + 'trace_id' => (string) $traceId, + 'level' => (string) $level, + 'body' => $message, + ]; + } + + public function flush(): ?EventId + { + if (empty($this->logs)) { + return null; + } + + $hub = SentrySdk::getCurrentHub(); + $event = Event::createLogs()->setLogs($this->logs); + + $this->logs = []; + + return $hub->captureEvent($event); + } + + /** + * @param array $tags + * + * @return array + */ + private function serializeTags(array $tags): array + { + $hub = SentrySdk::getCurrentHub(); + $client = $hub->getClient(); + + if ($client !== null) { + $options = $client->getOptions(); + + $defaultTags = [ + 'environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + ]; + + $release = $options->getRelease(); + if ($release !== null) { + $defaultTags['release'] = $release; + } + + $hub->configureScope(function (Scope $scope) use (&$defaultTags) { + $transaction = $scope->getTransaction(); + if ( + $transaction !== null + // Only include the transaction name if it has good quality + && $transaction->getMetadata()->getSource() !== TransactionSource::url() + ) { + $defaultTags['transaction'] = $transaction->getName(); + } + }); + + $tags = array_merge($defaultTags, $tags); + } + + // It's very important to sort the tags in order to obtain the same bucket key. + ksort($tags); + + return $tags; + } +} diff --git a/src/Serializer/EnvelopItems/LogsItem.php b/src/Serializer/EnvelopItems/LogsItem.php new file mode 100644 index 000000000..89021e72e --- /dev/null +++ b/src/Serializer/EnvelopItems/LogsItem.php @@ -0,0 +1,31 @@ + (string) $event->getType(), + 'content_type' => 'application/json', + ]; + + $payload = ''; + + $logs = $event->getLogs(); + foreach ($logs as $log) { + $payload .= \sprintf("%s\n%s", JSON::encode($header), JSON::encode($log)); + } + + return $payload; + } +} diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index c109e0336..6e33add8d 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -9,6 +9,7 @@ use Sentry\Options; use Sentry\Serializer\EnvelopItems\CheckInItem; use Sentry\Serializer\EnvelopItems\EventItem; +use Sentry\Serializer\EnvelopItems\LogsItem; use Sentry\Serializer\EnvelopItems\ProfileItem; use Sentry\Serializer\EnvelopItems\TransactionItem; use Sentry\Tracing\DynamicSamplingContext; @@ -77,6 +78,9 @@ public function serialize(Event $event): string case EventType::checkIn(): $items = CheckInItem::toEnvelopeItem($event); break; + case EventType::logs(): + $items = LogsItem::toEnvelopeItem($event); + break; } return \sprintf("%s\n%s", JSON::encode($envelopeHeader), $items); diff --git a/src/functions.php b/src/functions.php index 46a3d99e0..4eadf4944 100644 --- a/src/functions.php +++ b/src/functions.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\IntegrationInterface; +use Sentry\Logs\Logs; use Sentry\Metrics\Metrics; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; @@ -377,6 +378,11 @@ function continueTrace(string $sentryTrace, string $baggage): TransactionContext return TransactionContext::fromHeaders($sentryTrace, $baggage); } +function logger(): Logs +{ + return Logs::getInstance(); +} + /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ From 1c1359eb317aee7fa617635fe5c0576aae570487 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 22:59:28 +0200 Subject: [PATCH 02/45] Use new envelope item --- src/Serializer/EnvelopItems/LogsItem.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Serializer/EnvelopItems/LogsItem.php b/src/Serializer/EnvelopItems/LogsItem.php index 89021e72e..6d61cf5d6 100644 --- a/src/Serializer/EnvelopItems/LogsItem.php +++ b/src/Serializer/EnvelopItems/LogsItem.php @@ -14,18 +14,20 @@ class LogsItem implements EnvelopeItemInterface { public static function toEnvelopeItem(Event $event): string { + $logs = $event->getLogs(); + $header = [ 'type' => (string) $event->getType(), - 'content_type' => 'application/json', + 'item_count' => count($logs), + 'content_type' => 'application/vnd.sentry.items.log+json', ]; - $payload = ''; - - $logs = $event->getLogs(); - foreach ($logs as $log) { - $payload .= \sprintf("%s\n%s", JSON::encode($header), JSON::encode($log)); - } - - return $payload; + return \sprintf( + "%s\n%s", + JSON::encode($header), + JSON::encode([ + 'items' => $logs, + ]) + ); } } From f43e007fd5a2ada81f466b96cd76e26fc677aaaa Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 23:02:27 +0200 Subject: [PATCH 03/45] CS --- src/Serializer/EnvelopItems/LogsItem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serializer/EnvelopItems/LogsItem.php b/src/Serializer/EnvelopItems/LogsItem.php index 6d61cf5d6..97118d05c 100644 --- a/src/Serializer/EnvelopItems/LogsItem.php +++ b/src/Serializer/EnvelopItems/LogsItem.php @@ -18,7 +18,7 @@ public static function toEnvelopeItem(Event $event): string $header = [ 'type' => (string) $event->getType(), - 'item_count' => count($logs), + 'item_count' => \count($logs), 'content_type' => 'application/vnd.sentry.items.log+json', ]; From 5f256856bf567bf396c03c5ba6b0bb95ef0ba37d Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 23:02:35 +0200 Subject: [PATCH 04/45] Add all log levels --- src/Logs/LogLevel.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Logs/LogLevel.php b/src/Logs/LogLevel.php index 6e61f2849..2d8835578 100644 --- a/src/Logs/LogLevel.php +++ b/src/Logs/LogLevel.php @@ -21,11 +21,36 @@ private function __construct(string $value) $this->value = $value; } + public static function trace(): self + { + return self::getInstance('trace'); + } + + public static function debug(): self + { + return self::getInstance('debug'); + } + public static function info(): self { return self::getInstance('info'); } + public static function warn(): self + { + return self::getInstance('warn'); + } + + public static function error(): self + { + return self::getInstance('error'); + } + + public static function fatal(): self + { + return self::getInstance('fatal'); + } + public function __toString(): string { return $this->value; From 3dde07e107f6f3112b7c53b3adddde8a157ba27d Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 23:27:34 +0200 Subject: [PATCH 05/45] Add some default attributes --- src/Logs/LogsAggregator.php | 82 +++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index d5e6ee202..42979baee 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -4,11 +4,11 @@ namespace Sentry\Logs; +use Sentry\Client; use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; use Sentry\State\Scope; -use Sentry\Tracing\TransactionSource; /** * @internal @@ -30,67 +30,77 @@ public function add( $traceId = $span->getTraceId(); } - $this->logs[] = [ + $log = [ 'timestamp' => $timestamp, 'trace_id' => (string) $traceId, 'level' => (string) $level, 'body' => $message, + 'attributes' => [], ]; - } - - public function flush(): ?EventId - { - if (empty($this->logs)) { - return null; - } $hub = SentrySdk::getCurrentHub(); - $event = Event::createLogs()->setLogs($this->logs); - - $this->logs = []; - - return $hub->captureEvent($event); - } + $client = $hub->getClient(); - /** - * @param array $tags - * - * @return array - */ - private function serializeTags(array $tags): array - { $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); if ($client !== null) { $options = $client->getOptions(); - $defaultTags = [ - 'environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + // @TODO add a proper attributes abstraction we can later re-use for spans + $defaultAttributes = [ + 'sentry.environment' => [ + 'type' => 'string', + 'value' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + ], + 'sentry.sdk.name' => [ + 'type' => 'string', + // @FIXME Won't work for Laravel & Symfony + 'value' => Client::SDK_IDENTIFIER, + ], + 'sentry.sdk.version' => [ + 'type' => 'string', + // @FIXME Won't work for Laravel & Symfony + 'value' => Client::SDK_VERSION, + ], + // @TODO Add a `server.address` attribute, same value as `server_name` on errors ]; $release = $options->getRelease(); if ($release !== null) { - $defaultTags['release'] = $release; + $defaultAttributes['sentry.release'] = [ + 'type' => 'string', + 'value' => $options->getRelease(), + ]; } $hub->configureScope(function (Scope $scope) use (&$defaultTags) { - $transaction = $scope->getTransaction(); - if ( - $transaction !== null - // Only include the transaction name if it has good quality - && $transaction->getMetadata()->getSource() !== TransactionSource::url() - ) { - $defaultTags['transaction'] = $transaction->getName(); + $span = $scope->getSpan(); + if ($span !== null) { + $defaultAttributes['sentry.trace.parent_span_id'] = [ + 'type' => 'string', + 'value' => $span->getSpanId(), + ]; } }); - $tags = array_merge($defaultTags, $tags); + $log['attributes'] = $defaultAttributes; + } + + $this->logs[] = $log; + } + + public function flush(): ?EventId + { + if (empty($this->logs)) { + return null; } - // It's very important to sort the tags in order to obtain the same bucket key. - ksort($tags); + $hub = SentrySdk::getCurrentHub(); + $event = Event::createLogs()->setLogs($this->logs); - return $tags; + $this->logs = []; + + return $hub->captureEvent($event); } } From 72a927668f6b5bdce3d104c156d7eba012ec3bb2 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 23:40:05 +0200 Subject: [PATCH 06/45] Fix span id --- src/Logs/LogsAggregator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 42979baee..9d99840a1 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -74,12 +74,12 @@ public function add( ]; } - $hub->configureScope(function (Scope $scope) use (&$defaultTags) { + $hub->configureScope(function (Scope $scope) use (&$defaultAttributes) { $span = $scope->getSpan(); if ($span !== null) { $defaultAttributes['sentry.trace.parent_span_id'] = [ 'type' => 'string', - 'value' => $span->getSpanId(), + 'value' => (string) $span->getSpanId(), ]; } }); From 9720cc6b092319a3671fd2f74134a5a769b50921 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Wed, 16 Apr 2025 23:56:07 +0200 Subject: [PATCH 07/45] Expose more log levles --- src/Logs/Logs.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index b639d64bc..70a1105fd 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -32,11 +32,36 @@ public static function getInstance(): self return self::$instance; } + public function trace(string $message): void + { + $this->aggregator->add(LogLevel::trace(), $message); + } + + public function debug(string $message): void + { + $this->aggregator->add(LogLevel::debug(), $message); + } + public function info(string $message): void { $this->aggregator->add(LogLevel::info(), $message); } + public function warn(string $message): void + { + $this->aggregator->add(LogLevel::warn(), $message); + } + + public function error(string $message): void + { + $this->aggregator->add(LogLevel::error(), $message); + } + + public function fatal(string $message): void + { + $this->aggregator->add(LogLevel::fatal(), $message); + } + public function flush(): ?EventId { return $this->aggregator->flush(); From 478a2e797077af0250534654a66555703b7b044c Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 17:04:14 +0200 Subject: [PATCH 08/45] Add new data category for logs event --- src/Client.php | 14 ++++++++++++++ src/Transport/RateLimiter.php | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/Client.php b/src/Client.php index 23c551846..20ebbeeec 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,6 +8,7 @@ use Psr\Log\NullLogger; use Sentry\Integration\IntegrationInterface; use Sentry\Integration\IntegrationRegistry; +use Sentry\Logs\Log; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\State\Scope; @@ -409,6 +410,19 @@ private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event return ($this->options->getBeforeSendTransactionCallback())($event, $hint); case EventType::checkIn(): return ($this->options->getBeforeSendCheckInCallback())($event, $hint); + case EventType::logs(): + $logs = array_filter(array_map(function (Log $log): ?Log { + // @TODO: Should we emit a log that we dropped a log item? + return ($this->options->getBeforeSendLogCallback())($log); + }, $event->getLogs())); + + if (empty($logs)) { + return null; + } + + $event->setLogs($logs); + + return $event; default: return $event; } diff --git a/src/Transport/RateLimiter.php b/src/Transport/RateLimiter.php index 9a3d6d0f5..6370ed9ad 100644 --- a/src/Transport/RateLimiter.php +++ b/src/Transport/RateLimiter.php @@ -16,6 +16,11 @@ final class RateLimiter */ private const DATA_CATEGORY_ERROR = 'error'; + /** + * @var string + */ + private const DATA_CATEGORY_LOG_ITEM = 'log_item'; + /** * The name of the header to look at to know the rate limits for the events * categories supported by the server. @@ -106,6 +111,8 @@ public function getDisabledUntil(EventType $eventType): int if ($eventType === EventType::event()) { $category = self::DATA_CATEGORY_ERROR; + } elseif ($eventType === EventType::logs()) { + $category = self::DATA_CATEGORY_LOG_ITEM; } return max($this->rateLimits['all'] ?? 0, $this->rateLimits[$category] ?? 0); From ad9a7a36ff7ff19df926d69ee0601a62ea248985 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 17:04:31 +0200 Subject: [PATCH 09/45] Add options for logs --- phpstan-baseline.neon | 10 ++++++++++ src/Options.php | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5a9fe9f8..b3b5c4574 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -80,6 +80,11 @@ parameters: count: 1 path: src/Options.php + - + message: "#^Method Sentry\\\\Options\\:\\:getBeforeSendLogCallback\\(\\) should return callable\\(Sentry\\\\Logs\\\\Log\\)\\: \\(Sentry\\\\Logs\\\\Log\\|null\\) but returns mixed\\.$#" + count: 1 + path: src/Options.php + - message: "#^Method Sentry\\\\Options\\:\\:getBeforeSendMetricsCallback\\(\\) should return callable\\(Sentry\\\\Event, Sentry\\\\EventHint\\|null\\)\\: \\(Sentry\\\\Event\\|null\\) but returns mixed\\.$#" count: 1 @@ -105,6 +110,11 @@ parameters: count: 1 path: src/Options.php + - + message: "#^Method Sentry\\\\Options\\:\\:getEnableLogs\\(\\) should return bool but returns mixed\\.$#" + count: 1 + path: src/Options.php + - message: "#^Method Sentry\\\\Options\\:\\:getEnableTracing\\(\\) should return bool\\|null but returns mixed\\.$#" count: 1 diff --git a/src/Options.php b/src/Options.php index eb4ffb1f1..bb8445e4d 100644 --- a/src/Options.php +++ b/src/Options.php @@ -9,6 +9,7 @@ use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\ErrorListenerIntegration; use Sentry\Integration\IntegrationInterface; +use Sentry\Logs\Log; use Sentry\Transport\TransportInterface; use Symfony\Component\OptionsResolver\Options as SymfonyOptions; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -1112,6 +1113,39 @@ public function setTracesSampler(?callable $sampler): self return $this; } + /** + * Sets if logs should be enabled or not. + * + * @param bool|null $enableLogs Boolean if logs should be enabled or not + */ + public function setEnableLogs(?bool $enableLogs): self + { + $options = array_merge($this->options, ['enable_tracing' => $enableLogs]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + + /** + * Gets if logs is enabled or not. + */ + public function getEnableLogs(): bool + { + return $this->options['enable_logs'] ?? false; + } + + /** + * Gets a callback that will be invoked before an log is sent to the server. + * If `null` is returned it won't be sent. + * + * @psalm-return callable(Log): ?Log + */ + public function getBeforeSendLogCallback(): callable + { + return $this->options['before_send_log']; + } + /** * Configures the options of the client. * @@ -1187,6 +1221,10 @@ private function configureOptions(OptionsResolver $resolver): void 'capture_silenced_errors' => false, 'max_request_body_size' => 'medium', 'class_serializers' => [], + 'enable_logs' => false, + 'before_send_log' => static function (Log $log): Log { + return $log; + }, ]); $resolver->setAllowedTypes('prefixes', 'string[]'); @@ -1231,6 +1269,8 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('capture_silenced_errors', 'bool'); $resolver->setAllowedTypes('max_request_body_size', 'string'); $resolver->setAllowedTypes('class_serializers', 'array'); + $resolver->setAllowedTypes('enable_logs', 'bool'); + $resolver->setAllowedTypes('before_send_log', 'callable'); $resolver->setAllowedValues('max_request_body_size', ['none', 'never', 'small', 'medium', 'always']); $resolver->setAllowedValues('dsn', \Closure::fromCallable([$this, 'validateDsnOption'])); From bb89b44897045ec5f544840bfe3fc3e91cd3a1fd Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 17:04:36 +0200 Subject: [PATCH 10/45] CS --- src/Logs/LogLevel.php | 3 +++ src/functions.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Logs/LogLevel.php b/src/Logs/LogLevel.php index 2d8835578..5b12dc3d0 100644 --- a/src/Logs/LogLevel.php +++ b/src/Logs/LogLevel.php @@ -4,6 +4,9 @@ namespace Sentry\Logs; +/** + * @see: https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-level + */ class LogLevel { /** diff --git a/src/functions.php b/src/functions.php index 4eadf4944..8f5915f35 100644 --- a/src/functions.php +++ b/src/functions.php @@ -378,6 +378,9 @@ function continueTrace(string $sentryTrace, string $baggage): TransactionContext return TransactionContext::fromHeaders($sentryTrace, $baggage); } +/** + * Get the Sentry Logs client. + */ function logger(): Logs { return Logs::getInstance(); From f2a6d211ca12c9a21497d998a65baa890fd4c689 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 17:05:02 +0200 Subject: [PATCH 11/45] =?UTF-8?q?Refactor=20logs=20to=20use=20it=E2=80=99s?= =?UTF-8?q?=20own=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Event.php | 11 ++++- src/Logs/Log.php | 91 +++++++++++++++++++++++++++++++++++++ src/Logs/LogsAggregator.php | 85 +++++++++++----------------------- 3 files changed, 127 insertions(+), 60 deletions(-) create mode 100644 src/Logs/Log.php diff --git a/src/Event.php b/src/Event.php index 67f5f3407..b2c497232 100644 --- a/src/Event.php +++ b/src/Event.php @@ -6,6 +6,7 @@ use Sentry\Context\OsContext; use Sentry\Context\RuntimeContext; +use Sentry\Logs\Log; use Sentry\Profiling\Profile; use Sentry\Tracing\Span; @@ -62,9 +63,9 @@ final class Event private $checkIn; /** - * @var array|null + * @var Log[] */ - private $logs; + private $logs = []; /** * @var string|null The name of the server (e.g. the host name) @@ -378,11 +379,17 @@ public function setCheckIn(?CheckIn $checkIn): self return $this; } + /** + * @return Log[] + */ public function getLogs(): array { return $this->logs; } + /** + * @param Log[] $logs + */ public function setLogs(array $logs): self { $this->logs = $logs; diff --git a/src/Logs/Log.php b/src/Logs/Log.php new file mode 100644 index 000000000..58955ad13 --- /dev/null +++ b/src/Logs/Log.php @@ -0,0 +1,91 @@ + + * @phpstan-type LogEnvelopeItem array{ + * timestamp: int|float, + * trace_id: string, + * level: string, + * body: string, + * attributes: LogAttributes + * } + */ +class Log implements \JsonSerializable +{ + /** + * @var int + */ + private $timestamp; + + /** + * @var string + */ + private $traceId; + + /** + * @var LogLevel + */ + private $level; + + /** + * @var string + */ + private $body; + + /** + * @var LogAttributes + */ + private $attributes; + + /** + * @param LogAttributes $attributes + */ + public function __construct( + int $timestamp, + string $traceId, + LogLevel $level, + string $body, + array $attributes = [] + ) { + $this->timestamp = $timestamp; + $this->traceId = $traceId; + $this->level = $level; + $this->body = $body; + $this->attributes = $attributes; + } + + /** + * @param mixed $value + */ + public function setAttribute(string $key, $value, string $type = 'string'): self + { + $this->attributes[$key] = [ + 'type' => $type, + 'value' => $value, + ]; + + return $this; + } + + /** + * @return LogEnvelopeItem + */ + public function jsonSerialize(): array + { + return [ + 'timestamp' => $this->timestamp, + 'trace_id' => $this->traceId, + 'level' => (string) $this->level, + 'body' => $this->body, + 'attributes' => $this->attributes, + ]; + } +} diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 9d99840a1..104c47aaa 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -8,83 +8,52 @@ use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; -use Sentry\State\Scope; /** * @internal */ final class LogsAggregator { + /** + * @var Log[] + */ private $logs = []; public function add( LogLevel $level, - string $message, + string $message ): void { $timestamp = time(); - $traceId = null; $hub = SentrySdk::getCurrentHub(); - $span = $hub->getSpan(); - if ($span !== null) { - $traceId = $span->getTraceId(); + $client = $hub->getClient(); + + // There is no need to continue if there is no client or if logs are disabled + // @TODO: This might needs to be re-evaluated when we send logs to allow loggin to start before init'ing the client + if ($client === null || !$client->getOptions()->getEnableLogs()) { + return; } - $log = [ - 'timestamp' => $timestamp, - 'trace_id' => (string) $traceId, - 'level' => (string) $level, - 'body' => $message, - 'attributes' => [], - ]; + $span = $hub->getSpan(); + $traceId = $span !== null ? $span->getTraceId() : null; - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $options = $client->getOptions(); - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + // @TODO add a proper attributes abstraction we can later re-use for spans + // @TODO Add a `server.address` attribute, same value as `server_name` on errors + // @FIXME The SDK name and version won't work for Laravel & Symfony and other SDKs, needs to be more flexible + $log = (new Log($timestamp, (string) $traceId, $level, $message)) + ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) + ->setAttribute('sentry.sdk.name', Client::SDK_IDENTIFIER) + ->setAttribute('sentry.sdk.version', Client::SDK_VERSION); - if ($client !== null) { - $options = $client->getOptions(); - - // @TODO add a proper attributes abstraction we can later re-use for spans - $defaultAttributes = [ - 'sentry.environment' => [ - 'type' => 'string', - 'value' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, - ], - 'sentry.sdk.name' => [ - 'type' => 'string', - // @FIXME Won't work for Laravel & Symfony - 'value' => Client::SDK_IDENTIFIER, - ], - 'sentry.sdk.version' => [ - 'type' => 'string', - // @FIXME Won't work for Laravel & Symfony - 'value' => Client::SDK_VERSION, - ], - // @TODO Add a `server.address` attribute, same value as `server_name` on errors - ]; - - $release = $options->getRelease(); - if ($release !== null) { - $defaultAttributes['sentry.release'] = [ - 'type' => 'string', - 'value' => $options->getRelease(), - ]; - } - - $hub->configureScope(function (Scope $scope) use (&$defaultAttributes) { - $span = $scope->getSpan(); - if ($span !== null) { - $defaultAttributes['sentry.trace.parent_span_id'] = [ - 'type' => 'string', - 'value' => (string) $span->getSpanId(), - ]; - } - }); - - $log['attributes'] = $defaultAttributes; + $release = $options->getRelease(); + if ($release !== null) { + $log->setAttribute('sentry.release', $release); + } + + if ($span !== null) { + $log->setAttribute('sentry.trace.parent_span_id', (string) $span->getSpanId()); } $this->logs[] = $log; From 7eaf2e4d04a5e0a84651ed01377bf4154dc5cb2b Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 22:08:50 +0200 Subject: [PATCH 12/45] Introduce LogAttribute object --- src/Logs/Log.php | 23 ++++++-------- src/Logs/LogAttribute.php | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/Logs/LogAttribute.php diff --git a/src/Logs/Log.php b/src/Logs/Log.php index 58955ad13..cff325057 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -5,17 +5,15 @@ namespace Sentry\Logs; /** - * @phpstan-type LogAttribute array{ - * type: string, - * value: mixed - * } - * @phpstan-type LogAttributes array + * @phpstan-import-type AttributeValue from LogAttribute + * @phpstan-import-type AttributeSerialized from LogAttribute + * * @phpstan-type LogEnvelopeItem array{ * timestamp: int|float, * trace_id: string, * level: string, * body: string, - * attributes: LogAttributes + * attributes: array * } */ class Log implements \JsonSerializable @@ -41,12 +39,12 @@ class Log implements \JsonSerializable private $body; /** - * @var LogAttributes + * @var array */ private $attributes; /** - * @param LogAttributes $attributes + * @param array $attributes */ public function __construct( int $timestamp, @@ -63,14 +61,11 @@ public function __construct( } /** - * @param mixed $value + * @param AttributeValue $value */ - public function setAttribute(string $key, $value, string $type = 'string'): self + public function setAttribute(string $key, $value): self { - $this->attributes[$key] = [ - 'type' => $type, - 'value' => $value, - ]; + $this->attributes[$key] = LogAttribute::fromValue($value); return $this; } diff --git a/src/Logs/LogAttribute.php b/src/Logs/LogAttribute.php new file mode 100644 index 000000000..d2092f707 --- /dev/null +++ b/src/Logs/LogAttribute.php @@ -0,0 +1,67 @@ +value = $value; + $this->type = $type; + } + + /** + * @param AttributeValue $value + */ + public static function fromValue($value): self + { + if (\is_bool($value)) { + return new self($value, 'boolean'); + } + + if (\is_int($value)) { + return new self($value, 'integer'); + } + + if (\is_float($value)) { + return new self($value, 'double'); + } + + return new self((string) $value, 'string'); + } + + /** + * @return AttributeSerialized + */ + public function jsonSerialize(): array + { + return [ + 'type' => $this->type, + 'value' => $this->value, + ]; + } +} From 081359a0ba7b30115e9fd63228b1d3c1ca4f25fa Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 7 May 2025 22:08:57 +0200 Subject: [PATCH 13/45] Document new options --- src/functions.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/functions.php b/src/functions.php index 8f5915f35..22ea89957 100644 --- a/src/functions.php +++ b/src/functions.php @@ -62,6 +62,8 @@ * traces_sample_rate?: float|int|null, * traces_sampler?: callable|null, * transport?: callable, + * enable_logs?: bool, + * before_send_log?: callable, * } $options The client options */ function init(array $options = []): void From 700359e1544a2f9d192df6b3289b4be9886557eb Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Fri, 9 May 2025 17:22:04 +0200 Subject: [PATCH 14/45] Use micro seconds --- src/Logs/LogsAggregator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 104c47aaa..4920a6307 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -23,7 +23,7 @@ public function add( LogLevel $level, string $message ): void { - $timestamp = time(); + $timestamp = microtime(true); $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); From f79da3cf6ad3b4f7ecaa97052a053defd6d6b433 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Fri, 9 May 2025 18:47:54 +0200 Subject: [PATCH 15/45] CS --- src/Logs/Log.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Logs/Log.php b/src/Logs/Log.php index cff325057..5194da63f 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -19,7 +19,7 @@ class Log implements \JsonSerializable { /** - * @var int + * @var float */ private $timestamp; @@ -47,7 +47,7 @@ class Log implements \JsonSerializable * @param array $attributes */ public function __construct( - int $timestamp, + float $timestamp, string $traceId, LogLevel $level, string $body, From 57100ee69857e13e3fe06cffe4ec7b800f617504 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 12 May 2025 10:38:47 +0200 Subject: [PATCH 16/45] CS --- src/Logs/LogAttribute.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Logs/LogAttribute.php b/src/Logs/LogAttribute.php index d2092f707..56351a7a8 100644 --- a/src/Logs/LogAttribute.php +++ b/src/Logs/LogAttribute.php @@ -8,9 +8,9 @@ * @phpstan-type AttributeType 'string'|'boolean'|'integer'|'double' * @phpstan-type AttributeValue string|bool|int|float * @phpstan-type AttributeSerialized array{ - * type: AttributeType, - * value: AttributeValue - * } + * type: AttributeType, + * value: AttributeValue + * } */ class LogAttribute implements \JsonSerializable { @@ -35,7 +35,7 @@ public function __construct($value, string $type) } /** - * @param AttributeValue $value + * @param AttributeValue|\Stringable $value */ public static function fromValue($value): self { From 2248913acd2e0e9fddd60ef687e434418ad12730 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 12 May 2025 10:56:28 +0200 Subject: [PATCH 17/45] Allow user to pass values and attributes --- src/Logs/Logs.php | 60 +++++++++++++++++++++++++++++-------- src/Logs/LogsAggregator.php | 36 +++++++++++++++++----- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index 70a1105fd..ddcd6ac26 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -6,6 +6,9 @@ use Sentry\EventId; +/** + * @phpstan-import-type AttributeValue from LogAttribute + */ class Logs { /** @@ -32,36 +35,69 @@ public static function getInstance(): self return self::$instance; } - public function trace(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function trace(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::trace(), $message); + $this->aggregator->add(LogLevel::trace(), $message, $values, $attributes); } - public function debug(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function debug(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::debug(), $message); + $this->aggregator->add(LogLevel::debug(), $message, $values, $attributes); } - public function info(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function info(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::info(), $message); + $this->aggregator->add(LogLevel::info(), $message, $values, $attributes); } - public function warn(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function warn(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::warn(), $message); + $this->aggregator->add(LogLevel::warn(), $message, $values, $attributes); } - public function error(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function error(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::error(), $message); + $this->aggregator->add(LogLevel::error(), $message, $values, $attributes); } - public function fatal(string $message): void + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ + public function fatal(string $message, array $values = [], array $attributes = []): void { - $this->aggregator->add(LogLevel::fatal(), $message); + $this->aggregator->add(LogLevel::fatal(), $message, $values, $attributes); } + /** + * Flush the captured logs and send them to Sentry. + */ public function flush(): ?EventId { return $this->aggregator->flush(); diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 4920a6307..c6277b2a3 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -10,6 +10,8 @@ use Sentry\SentrySdk; /** + * @phpstan-import-type AttributeValue from LogAttribute + * * @internal */ final class LogsAggregator @@ -19,9 +21,16 @@ final class LogsAggregator */ private $logs = []; + /** + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log + */ public function add( LogLevel $level, - string $message + string $message, + array $values = [], + array $attributes = [] ): void { $timestamp = microtime(true); @@ -29,7 +38,7 @@ public function add( $client = $hub->getClient(); // There is no need to continue if there is no client or if logs are disabled - // @TODO: This might needs to be re-evaluated when we send logs to allow loggin to start before init'ing the client + // @TODO: This might needs to be re-evaluated when we send logs to allow logging to start before init'ing the client if ($client === null || !$client->getOptions()->getEnableLogs()) { return; } @@ -39,23 +48,34 @@ public function add( $options = $client->getOptions(); - // @TODO add a proper attributes abstraction we can later re-use for spans - // @TODO Add a `server.address` attribute, same value as `server_name` on errors // @FIXME The SDK name and version won't work for Laravel & Symfony and other SDKs, needs to be more flexible - $log = (new Log($timestamp, (string) $traceId, $level, $message)) + $log = (new Log($timestamp, (string) $traceId, $level, vsprintf($message, $values))) + ->setAttribute('sentry.message.template', $message) ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) ->setAttribute('sentry.sdk.name', Client::SDK_IDENTIFIER) ->setAttribute('sentry.sdk.version', Client::SDK_VERSION); - $release = $options->getRelease(); - if ($release !== null) { - $log->setAttribute('sentry.release', $release); + foreach ($values as $key => $value) { + $log->setAttribute("sentry.message.parameter.{$key}", $value); + } + + foreach ($attributes as $key => $value) { + $log->setAttribute($key, $value); } if ($span !== null) { $log->setAttribute('sentry.trace.parent_span_id', (string) $span->getSpanId()); } + // @TODO: Do we want to add the following attributes when we send the log rather then when we create the log? + if ($options->getServerName() !== null) { + $log->setAttribute('sentry.server.address', $options->getServerName()); + } + + if ($options->getRelease() !== null) { + $log->setAttribute('sentry.release', $options->getRelease()); + } + $this->logs[] = $log; } From 629f86af1e8ff4e818e7713e652aac9b0b2c95e2 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 12 May 2025 11:29:24 +0200 Subject: [PATCH 18/45] Add some initial tests --- tests/Logs/LogAttributeTest.php | 84 +++++++++++++++++++++++++++++++ tests/Logs/LogTest.php | 41 ++++++++++++++++ tests/Logs/LogsTest.php | 87 +++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 tests/Logs/LogAttributeTest.php create mode 100644 tests/Logs/LogTest.php create mode 100644 tests/Logs/LogsTest.php diff --git a/tests/Logs/LogAttributeTest.php b/tests/Logs/LogAttributeTest.php new file mode 100644 index 000000000..7ebac9bc6 --- /dev/null +++ b/tests/Logs/LogAttributeTest.php @@ -0,0 +1,84 @@ +expectException(\Error::class); + } + + $this->assertEquals($expected, LogAttribute::fromValue($value)->jsonSerialize()); + } + + public static function fromValueDataProvider(): \Generator + { + yield [ + 'foo', + [ + 'type' => 'string', + 'value' => 'foo', + ], + ]; + + yield [ + 123, + [ + 'type' => 'integer', + 'value' => 123, + ], + ]; + + yield [ + 123.33, + [ + 'type' => 'double', + 'value' => 123.33, + ], + ]; + + yield [ + true, + [ + 'type' => 'boolean', + 'value' => true, + ], + ]; + + yield [ + new class { + public function __toString(): string + { + return 'foo'; + } + }, + [ + 'type' => 'string', + 'value' => 'foo', + ], + ]; + + yield [ + new class {}, // not stringable or scalar + [], + true, + ]; + } +} diff --git a/tests/Logs/LogTest.php b/tests/Logs/LogTest.php new file mode 100644 index 000000000..e5d2a6526 --- /dev/null +++ b/tests/Logs/LogTest.php @@ -0,0 +1,41 @@ + LogAttribute::fromValue('bar'), + ]); + + $serialized = json_decode((string) json_encode($log), true); + + $this->assertEquals([ + 'timestamp' => $timestamp, + 'trace_id' => '123', + 'level' => 'debug', + 'body' => 'foo', + 'attributes' => [ + 'foo' => [ + 'type' => 'string', + 'value' => 'bar', + ], + ], + ], $serialized); + } +} diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php new file mode 100644 index 000000000..964d5857b --- /dev/null +++ b/tests/Logs/LogsTest.php @@ -0,0 +1,87 @@ +createMock(ClientInterface::class); + $client->expects($this->any()) + ->method('getOptions') + ->willReturn(new Options([ + 'dsn' => 'https://public@example.com/1', + 'enable_logs' => false, + ])); + + $client->expects($this->never()) + ->method('captureEvent'); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); + + logger()->info('Some info message'); + + $this->assertNull(logger()->flush()); + } + + public function testLogSentWhenEnabled(): void + { + $this->assertEvent(function (Event $event) { + $this->assertCount(1, $event->getLogs()); + + $logItem = $event->getLogs()[0]->jsonSerialize(); + + $this->assertEquals(LogLevel::info(), $logItem['level']); + $this->assertEquals('Some info message', $logItem['body']); + }); + + logger()->info('Some info message'); + + $this->assertNotNull(logger()->flush()); + } + + /** + * @param callable(Event): void $assert + */ + private function assertEvent(callable $assert): void + { + /** @var TransportInterface&MockObject $transport */ + $transport = $this->createMock(TransportInterface::class); + $transport->expects($this->once()) + ->method('send') + ->with($this->callback(function (Event $event) use ($assert): bool { + $assert($event); + + return true; + })) + ->willReturnCallback(static function (Event $event): Result { + return new Result(ResultStatus::success(), $event); + }); + + $client = ClientBuilder::create([ + 'enable_logs' => true, + ])->setTransport($transport)->getClient(); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); + } +} From bd543986f6eb7a9930bcdbdd19c83f511789e22d Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 12 May 2025 11:44:02 +0200 Subject: [PATCH 19/45] Add a PSR logger that logs to logs --- src/Logger/LogsLogger.php | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Logger/LogsLogger.php diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php new file mode 100644 index 000000000..663482068 --- /dev/null +++ b/src/Logger/LogsLogger.php @@ -0,0 +1,48 @@ +fatal((string) $message, [], $context); + break; + case 'error': + // @phpstan-ignore-next-line + Logs::getInstance()->error((string) $message, [], $context); + break; + case 'warning': + // @phpstan-ignore-next-line + Logs::getInstance()->warn((string) $message, [], $context); + break; + case 'debug': + // @phpstan-ignore-next-line + Logs::getInstance()->debug((string) $message, [], $context); + break; + default: + // @phpstan-ignore-next-line + Logs::getInstance()->info((string) $message, [], $context); + break; + } + } +} From 302b6bec3387c94817d398b831d920db99edaf70 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 13 May 2025 14:57:27 +0200 Subject: [PATCH 20/45] Use different expectations based on the PHP version --- tests/Logs/LogAttributeTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Logs/LogAttributeTest.php b/tests/Logs/LogAttributeTest.php index 7ebac9bc6..c791c3d62 100644 --- a/tests/Logs/LogAttributeTest.php +++ b/tests/Logs/LogAttributeTest.php @@ -22,7 +22,11 @@ final class LogAttributeTest extends TestCase public function testFromValue($value, array $expected, bool $expectError = false): void { if ($expectError) { - $this->expectException(\Error::class); + if (\PHP_VERSION_ID >= 70400) { + $this->expectException(\Error::class); + } else { + $this->expectError(); + } } $this->assertEquals($expected, LogAttribute::fromValue($value)->jsonSerialize()); @@ -76,7 +80,7 @@ public function __toString(): string ]; yield [ - new class {}, // not stringable or scalar + new \stdClass(), // not stringable nor a scalar [], true, ]; From f16a4f88cdca3efe3cc13e2be4fe0f53fcbee82d Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 13 May 2025 20:14:40 +0200 Subject: [PATCH 21/45] More refactorings --- src/Client.php | 27 ++++++++++++++++++- src/Logger/LogsLogger.php | 1 - src/Logs/Log.php | 19 ++++++------- src/Logs/LogAttribute.php | 20 +++++++++++--- src/Logs/LogsAggregator.php | 48 ++++++++++++++++----------------- tests/Logs/LogAttributeTest.php | 35 +++++++++++++++--------- tests/Logs/LogTest.php | 7 ++--- tests/Logs/LogsTest.php | 16 +++++++++++ 8 files changed, 118 insertions(+), 55 deletions(-) diff --git a/src/Client.php b/src/Client.php index f9c08addd..bc8500df8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -281,6 +281,7 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco $event->setSdkIdentifier($this->sdkIdentifier); $event->setSdkVersion($this->sdkVersion); + $event->setTags(array_merge($this->options->getTags(), $event->getTags())); if ($event->getServerName() === null) { @@ -344,6 +345,17 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco ), ['event' => $beforeSendCallback] ); + + return null; + } + + // We wait until here to set some default attributes on the log entries because in the meantime logs might have been filtered + foreach ($event->getLogs() as $log) { + $log->setAttribute('sentry.release', $this->options->getRelease()); + $log->setAttribute('sentry.sdk.name', $this->sdkIdentifier); + $log->setAttribute('sentry.sdk.version', $this->sdkVersion); + $log->setAttribute('sentry.environment', $this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT); + $log->setAttribute('sentry.server.address', $this->options->getServerName()); } return $event; @@ -412,7 +424,6 @@ private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event return ($this->options->getBeforeSendCheckInCallback())($event, $hint); case EventType::logs(): $logs = array_filter(array_map(function (Log $log): ?Log { - // @TODO: Should we emit a log that we dropped a log item? return ($this->options->getBeforeSendLogCallback())($log); }, $event->getLogs())); @@ -420,8 +431,20 @@ private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event return null; } + $logCountDifference = \count($event->getLogs()) - \count($logs); + $event->setLogs($logs); + if ($logCountDifference > 0) { + $this->logger->info( + \sprintf( + '%s logs will be discarded because the "before_send_log" callback returned "null" for them.', + $logCountDifference + ), + ['event' => $event] + ); + } + return $event; default: return $event; @@ -435,6 +458,8 @@ private function getBeforeSendCallbackName(Event $event): string return 'before_send_transaction'; case EventType::checkIn(): return 'before_send_check_in'; + case EventType::logs(): + return 'before_send_log'; default: return 'before_send'; } diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php index 663482068..fa7bad5f5 100644 --- a/src/Logger/LogsLogger.php +++ b/src/Logger/LogsLogger.php @@ -20,7 +20,6 @@ class LogsLogger extends AbstractLogger */ public function log($level, $message, array $context = []): void { - // @TODO: The $context might contain attributes we don't support yet (like arrays for example), should we ignore them? Type hint is basically mixed[]. switch ($level) { case 'emergency': case 'critical': diff --git a/src/Logs/Log.php b/src/Logs/Log.php index 5194da63f..4535aa279 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -41,31 +41,32 @@ class Log implements \JsonSerializable /** * @var array */ - private $attributes; + private $attributes = []; - /** - * @param array $attributes - */ public function __construct( float $timestamp, string $traceId, LogLevel $level, - string $body, - array $attributes = [] + string $body ) { $this->timestamp = $timestamp; $this->traceId = $traceId; $this->level = $level; $this->body = $body; - $this->attributes = $attributes; } /** - * @param AttributeValue $value + * @param mixed $value */ public function setAttribute(string $key, $value): self { - $this->attributes[$key] = LogAttribute::fromValue($value); + $attribute = $value instanceof LogAttribute + ? $value + : LogAttribute::tryFromValue($value); + + if ($attribute !== null) { + $this->attributes[$key] = $attribute; + } return $this; } diff --git a/src/Logs/LogAttribute.php b/src/Logs/LogAttribute.php index 56351a7a8..5f2d683fb 100644 --- a/src/Logs/LogAttribute.php +++ b/src/Logs/LogAttribute.php @@ -35,10 +35,14 @@ public function __construct($value, string $type) } /** - * @param AttributeValue|\Stringable $value + * @param mixed $value */ - public static function fromValue($value): self + public static function tryFromValue($value): ?self { + if ($value === null) { + return null; + } + if (\is_bool($value)) { return new self($value, 'boolean'); } @@ -51,7 +55,17 @@ public static function fromValue($value): self return new self($value, 'double'); } - return new self((string) $value, 'string'); + if (\is_string($value) || (\is_object($value) && method_exists($value, '__toString'))) { + $stringValue = (string) $value; + + if (empty($stringValue)) { + return null; + } + + return new self($stringValue, 'string'); + } + + return null; } /** diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index c6277b2a3..10209420b 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -4,10 +4,10 @@ namespace Sentry\Logs; -use Sentry\Client; use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; +use Sentry\State\Scope; /** * @phpstan-import-type AttributeValue from LogAttribute @@ -22,9 +22,9 @@ final class LogsAggregator private $logs = []; /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function add( LogLevel $level, @@ -38,42 +38,40 @@ public function add( $client = $hub->getClient(); // There is no need to continue if there is no client or if logs are disabled - // @TODO: This might needs to be re-evaluated when we send logs to allow logging to start before init'ing the client if ($client === null || !$client->getOptions()->getEnableLogs()) { return; } - $span = $hub->getSpan(); - $traceId = $span !== null ? $span->getTraceId() : null; + $scope = null; - $options = $client->getOptions(); + // This we push and pop a scope to get access to it because there is no accessor for the scope + $hub->configureScope(function (Scope $hubScope) use (&$scope) { + $scope = $hubScope; + }); + + \assert($scope !== null, 'The scope comes from the hub and cannot be null at this point.'); + + $traceId = $scope->getPropagationContext()->getTraceId(); // @FIXME The SDK name and version won't work for Laravel & Symfony and other SDKs, needs to be more flexible $log = (new Log($timestamp, (string) $traceId, $level, vsprintf($message, $values))) ->setAttribute('sentry.message.template', $message) - ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) - ->setAttribute('sentry.sdk.name', Client::SDK_IDENTIFIER) - ->setAttribute('sentry.sdk.version', Client::SDK_VERSION); + ->setAttribute('sentry.trace.parent_span_id', $hub->getSpan() ? $hub->getSpan()->getSpanId() : null); foreach ($values as $key => $value) { $log->setAttribute("sentry.message.parameter.{$key}", $value); } foreach ($attributes as $key => $value) { - $log->setAttribute($key, $value); - } - - if ($span !== null) { - $log->setAttribute('sentry.trace.parent_span_id', (string) $span->getSpanId()); - } - - // @TODO: Do we want to add the following attributes when we send the log rather then when we create the log? - if ($options->getServerName() !== null) { - $log->setAttribute('sentry.server.address', $options->getServerName()); - } - - if ($options->getRelease() !== null) { - $log->setAttribute('sentry.release', $options->getRelease()); + $attribute = LogAttribute::tryFromValue($value); + + if ($attribute === null) { + $client->getOptions()->getLoggerOrNullLogger()->info( + \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) + ); + } else { + $log->setAttribute($key, $attribute); + } } $this->logs[] = $log; diff --git a/tests/Logs/LogAttributeTest.php b/tests/Logs/LogAttributeTest.php index c791c3d62..d6219534d 100644 --- a/tests/Logs/LogAttributeTest.php +++ b/tests/Logs/LogAttributeTest.php @@ -14,22 +14,22 @@ final class LogAttributeTest extends TestCase { /** - * @param AttributeValue $value - * @param AttributeSerialized $expected + * @param AttributeValue $value + * @param AttributeSerialized|null $expected * * @dataProvider fromValueDataProvider */ - public function testFromValue($value, array $expected, bool $expectError = false): void + public function testFromValue($value, $expected): void { - if ($expectError) { - if (\PHP_VERSION_ID >= 70400) { - $this->expectException(\Error::class); - } else { - $this->expectError(); - } + $attribute = LogAttribute::tryFromValue($value); + + if ($expected === null) { + $this->assertNull($attribute); + + return; } - $this->assertEquals($expected, LogAttribute::fromValue($value)->jsonSerialize()); + $this->assertEquals($expected, $attribute->jsonSerialize()); } public static function fromValueDataProvider(): \Generator @@ -80,9 +80,18 @@ public function __toString(): string ]; yield [ - new \stdClass(), // not stringable nor a scalar - [], - true, + new class {}, + null, + ]; + + yield [ + new \stdClass(), + null, + ]; + + yield [ + ['key' => 'value'], + null, ]; } } diff --git a/tests/Logs/LogTest.php b/tests/Logs/LogTest.php index e5d2a6526..d5fe3bbe2 100644 --- a/tests/Logs/LogTest.php +++ b/tests/Logs/LogTest.php @@ -19,9 +19,10 @@ public function testJsonSerializesToExpected(): void { $timestamp = microtime(true); - $log = new Log($timestamp, '123', LogLevel::debug(), 'foo', [ - 'foo' => LogAttribute::fromValue('bar'), - ]); + $log = new Log($timestamp, '123', LogLevel::debug(), 'foo'); + + $log->setAttribute('foo', 'bar'); + $log->setAttribute('should-be-missing', ['foo' => 'bar']); $serialized = json_decode((string) json_encode($log), true); diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 964d5857b..18064c735 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -59,6 +59,22 @@ public function testLogSentWhenEnabled(): void $this->assertNotNull(logger()->flush()); } + public function testLogWithTemplate(): void + { + $this->assertEvent(function (Event $event) { + $this->assertCount(1, $event->getLogs()); + + $logItem = $event->getLogs()[0]->jsonSerialize(); + + $this->assertEquals(LogLevel::info(), $logItem['level']); + $this->assertEquals('Some info message', $logItem['body']); + }); + + logger()->info('Some %s message', ['info']); + + $this->assertNotNull(logger()->flush()); + } + /** * @param callable(Event): void $assert */ From b9f485d9e767ada88822420c393635edbfc63665 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 13 May 2025 20:42:22 +0200 Subject: [PATCH 22/45] Ensure we follow the propper SDK behavior --- src/Client.php | 46 ++++++++----------------------------- src/Logger/LogsLogger.php | 13 ++++++----- src/Logs/Log.php | 28 ++++++++++++++++++++++ src/Logs/LogAttribute.php | 5 ++++ src/Logs/LogsAggregator.php | 39 +++++++++++++++++++++++++++---- 5 files changed, 85 insertions(+), 46 deletions(-) diff --git a/src/Client.php b/src/Client.php index bc8500df8..6a5a4a4fc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,7 +8,6 @@ use Psr\Log\NullLogger; use Sentry\Integration\IntegrationInterface; use Sentry\Integration\IntegrationRegistry; -use Sentry\Logs\Log; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\State\Scope; @@ -256,6 +255,16 @@ public function getTransport(): TransportInterface return $this->transport; } + public function getSdkIdentifier(): string + { + return $this->sdkIdentifier; + } + + public function getSdkVersion(): string + { + return $this->sdkVersion; + } + /** * Assembles an event and prepares it to be sent of to Sentry. * @@ -349,15 +358,6 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco return null; } - // We wait until here to set some default attributes on the log entries because in the meantime logs might have been filtered - foreach ($event->getLogs() as $log) { - $log->setAttribute('sentry.release', $this->options->getRelease()); - $log->setAttribute('sentry.sdk.name', $this->sdkIdentifier); - $log->setAttribute('sentry.sdk.version', $this->sdkVersion); - $log->setAttribute('sentry.environment', $this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT); - $log->setAttribute('sentry.server.address', $this->options->getServerName()); - } - return $event; } @@ -422,30 +422,6 @@ private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event return ($this->options->getBeforeSendTransactionCallback())($event, $hint); case EventType::checkIn(): return ($this->options->getBeforeSendCheckInCallback())($event, $hint); - case EventType::logs(): - $logs = array_filter(array_map(function (Log $log): ?Log { - return ($this->options->getBeforeSendLogCallback())($log); - }, $event->getLogs())); - - if (empty($logs)) { - return null; - } - - $logCountDifference = \count($event->getLogs()) - \count($logs); - - $event->setLogs($logs); - - if ($logCountDifference > 0) { - $this->logger->info( - \sprintf( - '%s logs will be discarded because the "before_send_log" callback returned "null" for them.', - $logCountDifference - ), - ['event' => $event] - ); - } - - return $event; default: return $event; } @@ -458,8 +434,6 @@ private function getBeforeSendCallbackName(Event $event): string return 'before_send_transaction'; case EventType::checkIn(): return 'before_send_check_in'; - case EventType::logs(): - return 'before_send_log'; default: return 'before_send'; } diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php index fa7bad5f5..1f828442b 100644 --- a/src/Logger/LogsLogger.php +++ b/src/Logger/LogsLogger.php @@ -6,7 +6,8 @@ use Psr\Log\AbstractLogger; use Sentry\Logs\LogAttribute; -use Sentry\Logs\Logs; + +use function Sentry\logger; /** * @phpstan-import-type AttributeValue from LogAttribute @@ -24,23 +25,23 @@ public function log($level, $message, array $context = []): void case 'emergency': case 'critical': // @phpstan-ignore-next-line - Logs::getInstance()->fatal((string) $message, [], $context); + logger()->fatal((string) $message, [], $context); break; case 'error': // @phpstan-ignore-next-line - Logs::getInstance()->error((string) $message, [], $context); + logger()->error((string) $message, [], $context); break; case 'warning': // @phpstan-ignore-next-line - Logs::getInstance()->warn((string) $message, [], $context); + logger()->warn((string) $message, [], $context); break; case 'debug': // @phpstan-ignore-next-line - Logs::getInstance()->debug((string) $message, [], $context); + logger()->debug((string) $message, [], $context); break; default: // @phpstan-ignore-next-line - Logs::getInstance()->info((string) $message, [], $context); + logger()->info((string) $message, [], $context); break; } } diff --git a/src/Logs/Log.php b/src/Logs/Log.php index 4535aa279..deede246a 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -55,6 +55,26 @@ public function __construct( $this->body = $body; } + public function getTimestamp(): float + { + return $this->timestamp; + } + + public function getTraceId(): string + { + return $this->traceId; + } + + public function getLevel(): LogLevel + { + return $this->level; + } + + public function getBody(): string + { + return $this->body; + } + /** * @param mixed $value */ @@ -71,6 +91,14 @@ public function setAttribute(string $key, $value): self return $this; } + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + /** * @return LogEnvelopeItem */ diff --git a/src/Logs/LogAttribute.php b/src/Logs/LogAttribute.php index 5f2d683fb..87cfee528 100644 --- a/src/Logs/LogAttribute.php +++ b/src/Logs/LogAttribute.php @@ -78,4 +78,9 @@ public function jsonSerialize(): array 'value' => $this->value, ]; } + + public function __toString(): string + { + return "{$this->value} ({$this->type})"; + } } diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 10209420b..24a011742 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -4,6 +4,7 @@ namespace Sentry\Logs; +use Sentry\Client; use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; @@ -53,27 +54,57 @@ public function add( $traceId = $scope->getPropagationContext()->getTraceId(); - // @FIXME The SDK name and version won't work for Laravel & Symfony and other SDKs, needs to be more flexible + $options = $client->getOptions(); + $log = (new Log($timestamp, (string) $traceId, $level, vsprintf($message, $values))) + ->setAttribute('sentry.release', $options->getRelease()) + ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) + ->setAttribute('sentry.server.address', $options->getServerName()) ->setAttribute('sentry.message.template', $message) ->setAttribute('sentry.trace.parent_span_id', $hub->getSpan() ? $hub->getSpan()->getSpanId() : null); + if ($client instanceof Client) { + $log->setAttribute('sentry.sdk.name', $client->getSdkIdentifier()); + $log->setAttribute('sentry.sdk.version', $client->getSdkVersion()); + } + foreach ($values as $key => $value) { $log->setAttribute("sentry.message.parameter.{$key}", $value); } + $logger = $options->getLogger(); + foreach ($attributes as $key => $value) { $attribute = LogAttribute::tryFromValue($value); if ($attribute === null) { - $client->getOptions()->getLoggerOrNullLogger()->info( - \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) - ); + if ($logger !== null) { + $logger->info( + \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) + ); + } } else { $log->setAttribute($key, $attribute); } } + $log = ($options->getBeforeSendLogCallback())($log); + + if ($log === null) { + if ($logger !== null) { + $logger->info( + 'Log will be discarded because the "before_send_log" callback returned "null".', + ['log' => $log] + ); + } + + return; + } + + if ($logger !== null) { + $logger->log((string) $log->getLevel(), $log->getBody(), $log->getAttributes()); + } + $this->logs[] = $log; } From e8f9025a30e7f42676bd8a0736229d9d7b8c6fe6 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 15:04:25 +0200 Subject: [PATCH 23/45] Refactor attributes to be re-usable --- .../Attribute.php} | 4 +- src/Attributes/AttributeBag.php | 42 +++++++++++++++++++ src/Logger/LogsLogger.php | 4 +- src/Logs/Log.php | 27 +++++------- src/Logs/Logs.php | 3 +- src/Logs/LogsAggregator.php | 6 ++- tests/Logs/LogAttributeTest.php | 8 ++-- tests/Logs/LogTest.php | 6 +-- 8 files changed, 70 insertions(+), 30 deletions(-) rename src/{Logs/LogAttribute.php => Attributes/Attribute.php} (95%) create mode 100644 src/Attributes/AttributeBag.php diff --git a/src/Logs/LogAttribute.php b/src/Attributes/Attribute.php similarity index 95% rename from src/Logs/LogAttribute.php rename to src/Attributes/Attribute.php index 87cfee528..f93648fd8 100644 --- a/src/Logs/LogAttribute.php +++ b/src/Attributes/Attribute.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sentry\Logs; +namespace Sentry\Attributes; /** * @phpstan-type AttributeType 'string'|'boolean'|'integer'|'double' @@ -12,7 +12,7 @@ * value: AttributeValue * } */ -class LogAttribute implements \JsonSerializable +class Attribute implements \JsonSerializable { /** * @var AttributeType diff --git a/src/Attributes/AttributeBag.php b/src/Attributes/AttributeBag.php new file mode 100644 index 000000000..d9baa77fd --- /dev/null +++ b/src/Attributes/AttributeBag.php @@ -0,0 +1,42 @@ + + */ + private $attributes = []; + + /** + * @param mixed $value + */ + public function set(string $key, $value): self + { + $attribute = $value instanceof Attribute + ? $value + : Attribute::tryFromValue($value); + + if ($attribute !== null) { + $this->attributes[$key] = $attribute; + } + + return $this; + } + + public function get(string $key): ?Attribute + { + return $this->attributes[$key] ?? null; + } + + /** + * @return array + */ + public function all(): array + { + return $this->attributes; + } +} diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php index 1f828442b..7a4a7bd60 100644 --- a/src/Logger/LogsLogger.php +++ b/src/Logger/LogsLogger.php @@ -5,12 +5,12 @@ namespace Sentry\Logger; use Psr\Log\AbstractLogger; -use Sentry\Logs\LogAttribute; +use Sentry\Attributes\Attribute; use function Sentry\logger; /** - * @phpstan-import-type AttributeValue from LogAttribute + * @phpstan-import-type AttributeValue from Attribute */ class LogsLogger extends AbstractLogger { diff --git a/src/Logs/Log.php b/src/Logs/Log.php index deede246a..448de664d 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -4,16 +4,16 @@ namespace Sentry\Logs; +use Sentry\Attributes\Attribute; +use Sentry\Attributes\AttributeBag; + /** - * @phpstan-import-type AttributeValue from LogAttribute - * @phpstan-import-type AttributeSerialized from LogAttribute - * * @phpstan-type LogEnvelopeItem array{ * timestamp: int|float, * trace_id: string, * level: string, * body: string, - * attributes: array + * attributes: array * } */ class Log implements \JsonSerializable @@ -39,9 +39,9 @@ class Log implements \JsonSerializable private $body; /** - * @var array + * @var AttributeBag */ - private $attributes = []; + private $attributes; public function __construct( float $timestamp, @@ -53,6 +53,7 @@ public function __construct( $this->traceId = $traceId; $this->level = $level; $this->body = $body; + $this->attributes = new AttributeBag(); } public function getTimestamp(): float @@ -80,23 +81,17 @@ public function getBody(): string */ public function setAttribute(string $key, $value): self { - $attribute = $value instanceof LogAttribute - ? $value - : LogAttribute::tryFromValue($value); - - if ($attribute !== null) { - $this->attributes[$key] = $attribute; - } + $this->attributes->set($key, $value); return $this; } /** - * @return array + * @return array */ public function getAttributes(): array { - return $this->attributes; + return $this->attributes->all(); } /** @@ -109,7 +104,7 @@ public function jsonSerialize(): array 'trace_id' => $this->traceId, 'level' => (string) $this->level, 'body' => $this->body, - 'attributes' => $this->attributes, + 'attributes' => $this->attributes->all(), ]; } } diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index ddcd6ac26..59bcc856e 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -4,10 +4,11 @@ namespace Sentry\Logs; +use Sentry\Attributes\Attribute; use Sentry\EventId; /** - * @phpstan-import-type AttributeValue from LogAttribute + * @phpstan-import-type AttributeValue from Attribute */ class Logs { diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 24a011742..73e648c4d 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -4,14 +4,16 @@ namespace Sentry\Logs; +use Sentry\Attributes\Attribute; use Sentry\Client; use Sentry\Event; use Sentry\EventId; +use Sentry\Logger\LogsLogger; use Sentry\SentrySdk; use Sentry\State\Scope; /** - * @phpstan-import-type AttributeValue from LogAttribute + * @phpstan-import-type AttributeValue from Attribute * * @internal */ @@ -75,7 +77,7 @@ public function add( $logger = $options->getLogger(); foreach ($attributes as $key => $value) { - $attribute = LogAttribute::tryFromValue($value); + $attribute = Attribute::tryFromValue($value); if ($attribute === null) { if ($logger !== null) { diff --git a/tests/Logs/LogAttributeTest.php b/tests/Logs/LogAttributeTest.php index d6219534d..56b49f6b8 100644 --- a/tests/Logs/LogAttributeTest.php +++ b/tests/Logs/LogAttributeTest.php @@ -5,11 +5,11 @@ namespace Sentry\Tests\Logs; use PHPUnit\Framework\TestCase; -use Sentry\Logs\LogAttribute; +use Sentry\Attributes\Attribute; /** - * @phpstan-import-type AttributeValue from LogAttribute - * @phpstan-import-type AttributeSerialized from LogAttribute + * @phpstan-import-type AttributeValue from Attribute + * @phpstan-import-type AttributeSerialized from Attribute */ final class LogAttributeTest extends TestCase { @@ -21,7 +21,7 @@ final class LogAttributeTest extends TestCase */ public function testFromValue($value, $expected): void { - $attribute = LogAttribute::tryFromValue($value); + $attribute = Attribute::tryFromValue($value); if ($expected === null) { $this->assertNull($attribute); diff --git a/tests/Logs/LogTest.php b/tests/Logs/LogTest.php index d5fe3bbe2..4dd5a223a 100644 --- a/tests/Logs/LogTest.php +++ b/tests/Logs/LogTest.php @@ -5,13 +5,13 @@ namespace Sentry\Tests\Logs; use PHPUnit\Framework\TestCase; +use Sentry\Attributes\Attribute; use Sentry\Logs\Log; -use Sentry\Logs\LogAttribute; use Sentry\Logs\LogLevel; /** - * @phpstan-import-type AttributeValue from LogAttribute - * @phpstan-import-type AttributeSerialized from LogAttribute + * @phpstan-import-type AttributeValue from Attribute + * @phpstan-import-type AttributeSerialized from Attribute */ final class LogTest extends TestCase { From 1b6bdcabed8414b56bcea7b1b7c1417e2b15f4ce Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 15:04:32 +0200 Subject: [PATCH 24/45] Prevent logging to ourself --- src/Logs/LogsAggregator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 73e648c4d..53af89fe9 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -103,7 +103,8 @@ public function add( return; } - if ($logger !== null) { + // We check if it's a `LogsLogger` to avoid a infinite loop where the logger is logging the logs it's writing + if ($logger !== null && !$logger instanceof LogsLogger) { $logger->log((string) $log->getLevel(), $log->getBody(), $log->getAttributes()); } From d62da8152390167439cdadb83a3e75a54eca8740 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 15:15:02 +0200 Subject: [PATCH 25/45] Increase API surface for attributes and tests --- src/Attributes/Attribute.php | 42 ++++++++++++- src/Attributes/AttributeBag.php | 24 ++++++++ src/Logs/Log.php | 13 ++-- src/Logs/LogsAggregator.php | 2 +- .../AttributeTest.php} | 60 +++++++++++++++++-- 5 files changed, 126 insertions(+), 15 deletions(-) rename tests/{Logs/LogAttributeTest.php => Attributes/AttributeTest.php} (52%) diff --git a/src/Attributes/Attribute.php b/src/Attributes/Attribute.php index f93648fd8..c3ff48355 100644 --- a/src/Attributes/Attribute.php +++ b/src/Attributes/Attribute.php @@ -34,6 +34,38 @@ public function __construct($value, string $type) $this->type = $type; } + /** + * @return AttributeType + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return AttributeValue + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + * + * @throws \InvalidArgumentException thrown when the value cannot be serialized as an attribute + */ + public static function fromValue($value): self + { + $attribute = self::tryFromValue($value); + + if ($attribute === null) { + throw new \InvalidArgumentException(\sprintf('Invalid attribute value, %s cannot be serialized', \gettype($value))); + } + + return $attribute; + } + /** * @param mixed $value */ @@ -71,7 +103,7 @@ public static function tryFromValue($value): ?self /** * @return AttributeSerialized */ - public function jsonSerialize(): array + public function toArray(): array { return [ 'type' => $this->type, @@ -79,6 +111,14 @@ public function jsonSerialize(): array ]; } + /** + * @return AttributeSerialized + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + public function __toString(): string { return "{$this->value} ({$this->type})"; diff --git a/src/Attributes/AttributeBag.php b/src/Attributes/AttributeBag.php index d9baa77fd..4611946f1 100644 --- a/src/Attributes/AttributeBag.php +++ b/src/Attributes/AttributeBag.php @@ -4,6 +4,10 @@ namespace Sentry\Attributes; +/** + * @phpstan-import-type AttributeValue from Attribute + * @phpstan-import-type AttributeSerialized from Attribute + */ class AttributeBag { /** @@ -39,4 +43,24 @@ public function all(): array { return $this->attributes; } + + /** + * @return array + */ + public function toArray(): array + { + return array_map(static function (Attribute $attribute) { + return $attribute->jsonSerialize(); + }, $this->attributes); + } + + /** + * @return array + */ + public function toSimpleArray(): array + { + return array_map(static function (Attribute $attribute) { + return $attribute->getValue(); + }, $this->attributes); + } } diff --git a/src/Logs/Log.php b/src/Logs/Log.php index 448de664d..61732d69e 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -76,6 +76,11 @@ public function getBody(): string return $this->body; } + public function attributes(): AttributeBag + { + return $this->attributes; + } + /** * @param mixed $value */ @@ -86,14 +91,6 @@ public function setAttribute(string $key, $value): self return $this; } - /** - * @return array - */ - public function getAttributes(): array - { - return $this->attributes->all(); - } - /** * @return LogEnvelopeItem */ diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 53af89fe9..272864350 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -105,7 +105,7 @@ public function add( // We check if it's a `LogsLogger` to avoid a infinite loop where the logger is logging the logs it's writing if ($logger !== null && !$logger instanceof LogsLogger) { - $logger->log((string) $log->getLevel(), $log->getBody(), $log->getAttributes()); + $logger->log((string) $log->getLevel(), $log->getBody(), $log->attributes()->toSimpleArray()); } $this->logs[] = $log; diff --git a/tests/Logs/LogAttributeTest.php b/tests/Attributes/AttributeTest.php similarity index 52% rename from tests/Logs/LogAttributeTest.php rename to tests/Attributes/AttributeTest.php index 56b49f6b8..3919e1ce0 100644 --- a/tests/Logs/LogAttributeTest.php +++ b/tests/Attributes/AttributeTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sentry\Tests\Logs; +namespace Sentry\Tests\Attributes; use PHPUnit\Framework\TestCase; use Sentry\Attributes\Attribute; @@ -11,7 +11,7 @@ * @phpstan-import-type AttributeValue from Attribute * @phpstan-import-type AttributeSerialized from Attribute */ -final class LogAttributeTest extends TestCase +final class AttributeTest extends TestCase { /** * @param AttributeValue $value @@ -23,13 +23,15 @@ public function testFromValue($value, $expected): void { $attribute = Attribute::tryFromValue($value); - if ($expected === null) { + if ($attribute === null || $expected === null) { $this->assertNull($attribute); return; } - $this->assertEquals($expected, $attribute->jsonSerialize()); + $this->assertEquals($expected, $attribute->toArray()); + $this->assertEquals($expected['type'], $attribute->getType()); + $this->assertEquals($expected['value'], $attribute->getValue()); } public static function fromValueDataProvider(): \Generator @@ -90,8 +92,56 @@ public function __toString(): string ]; yield [ - ['key' => 'value'], + [], null, ]; } + + public function testSerializeAsJson(): void + { + $attribute = Attribute::tryFromValue('foo'); + + $this->assertInstanceOf(Attribute::class, $attribute); + + $this->assertEquals( + ['type' => 'string', 'value' => 'foo'], + $attribute->jsonSerialize() + ); + + $this->assertEquals( + '{"type":"string","value":"foo"}', + json_encode($attribute) + ); + } + + public function testSerializeAsArray(): void + { + $attribute = Attribute::tryFromValue('foo'); + + $this->assertInstanceOf(Attribute::class, $attribute); + + $this->assertEquals( + ['type' => 'string', 'value' => 'foo'], + $attribute->toArray() + ); + } + + public function testSerializeAsString(): void + { + $attribute = Attribute::tryFromValue('foo'); + + $this->assertInstanceOf(Attribute::class, $attribute); + + $this->assertEquals( + 'foo (string)', + (string) $attribute + ); + } + + public function testFromValueFactoryMethod(): void + { + $this->expectException(\InvalidArgumentException::class); + + Attribute::fromValue([]); + } } From f2104594f4d213966d1b09af5aaa40f6f86a44ab Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 15:53:35 +0200 Subject: [PATCH 26/45] Add attribute bag tests --- src/Attributes/AttributeBag.php | 10 +++- tests/Attributes/AttributeBagTest.php | 68 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/Attributes/AttributeBagTest.php diff --git a/src/Attributes/AttributeBag.php b/src/Attributes/AttributeBag.php index 4611946f1..00cb45dc5 100644 --- a/src/Attributes/AttributeBag.php +++ b/src/Attributes/AttributeBag.php @@ -8,7 +8,7 @@ * @phpstan-import-type AttributeValue from Attribute * @phpstan-import-type AttributeSerialized from Attribute */ -class AttributeBag +class AttributeBag implements \JsonSerializable { /** * @var array @@ -54,6 +54,14 @@ public function toArray(): array }, $this->attributes); } + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } + /** * @return array */ diff --git a/tests/Attributes/AttributeBagTest.php b/tests/Attributes/AttributeBagTest.php new file mode 100644 index 000000000..397fc8cf5 --- /dev/null +++ b/tests/Attributes/AttributeBagTest.php @@ -0,0 +1,68 @@ +assertCount(0, $bag->all()); + + $bag->set('foo', 'bar'); + + $this->assertCount(1, $bag->all()); + $this->assertInstanceOf(Attribute::class, $bag->get('foo')); + + $this->assertNull($bag->get('non-existing')); + } + + public function testSerializeAsJson(): void + { + $bag = new AttributeBag(); + $bag->set('foo', 'bar'); + + $this->assertEquals( + ['foo' => ['type' => 'string', 'value' => 'bar']], + $bag->jsonSerialize() + ); + + $this->assertEquals( + '{"foo":{"type":"string","value":"bar"}}', + json_encode($bag) + ); + } + + public function testSerializeAsArray(): void + { + $bag = new AttributeBag(); + $bag->set('foo', 'bar'); + + $this->assertEquals( + ['foo' => ['type' => 'string', 'value' => 'bar']], + $bag->toArray() + ); + } + + public function testSerializeAsSimpleArray(): void + { + $bag = new AttributeBag(); + $bag->set('foo', 'bar'); + + $this->assertEquals( + ['foo' => 'bar'], + $bag->toSimpleArray() + ); + } +} From 2b23aeeef799a7958ed2e0b9993a284f0b903aeb Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 16:18:22 +0200 Subject: [PATCH 27/45] When sending an event, also send pending logs --- src/Client.php | 6 ++++ src/Logs/Logs.php | 10 ++++++ src/Logs/LogsAggregator.php | 14 ++++++++ .../EnvelopItems/EnvelopeItemInterface.php | 2 +- src/Serializer/EnvelopItems/LogsItem.php | 3 +- src/Serializer/EnvelopItems/ProfileItem.php | 6 ++-- .../EnvelopItems/TransactionItem.php | 3 +- src/Serializer/PayloadSerializer.php | 23 +++++++------ tests/Logs/LogsTest.php | 20 +++++++++++- tests/Serializer/PayloadSerializerTest.php | 32 +++++++++++++++++++ 10 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/Client.php b/src/Client.php index 6a5a4a4fc..76c1538da 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,6 +8,7 @@ use Psr\Log\NullLogger; use Sentry\Integration\IntegrationInterface; use Sentry\Integration\IntegrationRegistry; +use Sentry\Logs\Logs; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\State\Scope; @@ -358,6 +359,11 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco return null; } + // When we sent an non-logs event to Sentry, also flush the logs in the same envelope to prevent needing to make multiple requests + if ($event->getType() !== EventType::logs() && !\count($event->getLogs())) { + $event->setLogs(Logs::getInstance()->aggregator()->flushWithoutEvent()); + } + return $event; } diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index 59bcc856e..c1dc5ce1f 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -103,4 +103,14 @@ public function flush(): ?EventId { return $this->aggregator->flush(); } + + /** + * Get the logs aggregator. + * + * @internal + */ + public function aggregator(): LogsAggregator + { + return $this->aggregator; + } } diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 272864350..d7d233c0d 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -124,4 +124,18 @@ public function flush(): ?EventId return $hub->captureEvent($event); } + + /** + * @internal + * + * @return Log[] + */ + public function flushWithoutEvent(): array + { + $logs = $this->logs; + + $this->logs = []; + + return $logs; + } } diff --git a/src/Serializer/EnvelopItems/EnvelopeItemInterface.php b/src/Serializer/EnvelopItems/EnvelopeItemInterface.php index d2b7d3712..bf95f6e45 100644 --- a/src/Serializer/EnvelopItems/EnvelopeItemInterface.php +++ b/src/Serializer/EnvelopItems/EnvelopeItemInterface.php @@ -11,5 +11,5 @@ */ interface EnvelopeItemInterface { - public static function toEnvelopeItem(Event $event): string; + public static function toEnvelopeItem(Event $event): ?string; } diff --git a/src/Serializer/EnvelopItems/LogsItem.php b/src/Serializer/EnvelopItems/LogsItem.php index 97118d05c..431e67f9f 100644 --- a/src/Serializer/EnvelopItems/LogsItem.php +++ b/src/Serializer/EnvelopItems/LogsItem.php @@ -5,6 +5,7 @@ namespace Sentry\Serializer\EnvelopItems; use Sentry\Event; +use Sentry\EventType; use Sentry\Util\JSON; /** @@ -17,7 +18,7 @@ public static function toEnvelopeItem(Event $event): string $logs = $event->getLogs(); $header = [ - 'type' => (string) $event->getType(), + 'type' => (string) EventType::logs(), 'item_count' => \count($logs), 'content_type' => 'application/vnd.sentry.items.log+json', ]; diff --git a/src/Serializer/EnvelopItems/ProfileItem.php b/src/Serializer/EnvelopItems/ProfileItem.php index 646506478..512eae80f 100644 --- a/src/Serializer/EnvelopItems/ProfileItem.php +++ b/src/Serializer/EnvelopItems/ProfileItem.php @@ -13,7 +13,7 @@ */ class ProfileItem implements EnvelopeItemInterface { - public static function toEnvelopeItem(Event $event): string + public static function toEnvelopeItem(Event $event): ?string { $header = [ 'type' => 'profile', @@ -22,12 +22,12 @@ public static function toEnvelopeItem(Event $event): string $profile = $event->getSdkMetadata('profile'); if (!$profile instanceof Profile) { - return ''; + return null; } $payload = $profile->getFormattedData($event); if ($payload === null) { - return ''; + return null; } return \sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); diff --git a/src/Serializer/EnvelopItems/TransactionItem.php b/src/Serializer/EnvelopItems/TransactionItem.php index 679bd6bcd..9eb8eab22 100644 --- a/src/Serializer/EnvelopItems/TransactionItem.php +++ b/src/Serializer/EnvelopItems/TransactionItem.php @@ -5,6 +5,7 @@ namespace Sentry\Serializer\EnvelopItems; use Sentry\Event; +use Sentry\EventType; use Sentry\Serializer\Traits\BreadcrumbSeralizerTrait; use Sentry\Tracing\Span; use Sentry\Tracing\TransactionMetadata; @@ -28,7 +29,7 @@ class TransactionItem implements EnvelopeItemInterface public static function toEnvelopeItem(Event $event): string { $header = [ - 'type' => (string) $event->getType(), + 'type' => (string) EventType::transaction(), 'content_type' => 'application/json', ]; diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index 1e6142206..64fc84358 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -55,31 +55,30 @@ public function serialize(Event $event): string } } - $items = ''; + $items = []; switch ($event->getType()) { case EventType::event(): - $items = EventItem::toEnvelopeItem($event); + $items[] = EventItem::toEnvelopeItem($event); break; case EventType::transaction(): - $transactionItem = TransactionItem::toEnvelopeItem($event); + $items[] = TransactionItem::toEnvelopeItem($event); if ($event->getSdkMetadata('profile') !== null) { - $profileItem = ProfileItem::toEnvelopeItem($event); - if ($profileItem !== '') { - $items = \sprintf("%s\n%s", $transactionItem, $profileItem); - break; - } + $items[] = ProfileItem::toEnvelopeItem($event); } - $items = $transactionItem; break; case EventType::checkIn(): - $items = CheckInItem::toEnvelopeItem($event); + $items[] = CheckInItem::toEnvelopeItem($event); break; case EventType::logs(): - $items = LogsItem::toEnvelopeItem($event); + $items[] = LogsItem::toEnvelopeItem($event); break; } - return \sprintf("%s\n%s", JSON::encode($envelopeHeader), $items); + if ($event->getType() !== EventType::logs() && \count($event->getLogs())) { + $items[] = LogsItem::toEnvelopeItem($event); + } + + return \sprintf("%s\n%s", JSON::encode($envelopeHeader), implode("\n", array_filter($items))); } } diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 18064c735..c1c14076e 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -59,6 +59,22 @@ public function testLogSentWhenEnabled(): void $this->assertNotNull(logger()->flush()); } + public function testLogSentWithOtherEvents(): void + { + $client = $this->assertEvent(function (Event $event) { + $this->assertCount(1, $event->getLogs()); + + $logItem = $event->getLogs()[0]->jsonSerialize(); + + $this->assertEquals(LogLevel::info(), $logItem['level']); + $this->assertEquals('Some info message', $logItem['body']); + }); + + logger()->info('Some info message'); + + $this->assertNotNull($client->captureMessage('Some message')); + } + public function testLogWithTemplate(): void { $this->assertEvent(function (Event $event) { @@ -78,7 +94,7 @@ public function testLogWithTemplate(): void /** * @param callable(Event): void $assert */ - private function assertEvent(callable $assert): void + private function assertEvent(callable $assert): ClientInterface { /** @var TransportInterface&MockObject $transport */ $transport = $this->createMock(TransportInterface::class); @@ -99,5 +115,7 @@ private function assertEvent(callable $assert): void $hub = new Hub($client); SentrySdk::setCurrentHub($hub); + + return $client; } } diff --git a/tests/Serializer/PayloadSerializerTest.php b/tests/Serializer/PayloadSerializerTest.php index 1e8eca0fe..a324efb85 100644 --- a/tests/Serializer/PayloadSerializerTest.php +++ b/tests/Serializer/PayloadSerializerTest.php @@ -16,6 +16,8 @@ use Sentry\ExceptionDataBag; use Sentry\ExceptionMechanism; use Sentry\Frame; +use Sentry\Logs\Log; +use Sentry\Logs\LogLevel; use Sentry\MonitorConfig; use Sentry\MonitorSchedule; use Sentry\Options; @@ -408,5 +410,35 @@ public static function serializeAsEnvelopeDataProvider(): iterable TEXT , ]; + + $event = Event::createLogs(new EventId('fc9442f5aef34234bb22b9a615e30ccd')); + $event->setLogs([ + new Log(ClockMock::microtime(true), '21160e9b836d479f81611368b2aa3d2c', LogLevel::info(), 'A log message'), + ]); + + yield [ + $event, + <<setLogs([ + new Log(ClockMock::microtime(true), '21160e9b836d479f81611368b2aa3d2c', LogLevel::info(), 'A log message'), + ]); + + yield [ + $event, + << Date: Thu, 15 May 2025 16:34:51 +0200 Subject: [PATCH 28/45] CS --- tests/Serializer/PayloadSerializerTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Serializer/PayloadSerializerTest.php b/tests/Serializer/PayloadSerializerTest.php index a324efb85..e37bafe0c 100644 --- a/tests/Serializer/PayloadSerializerTest.php +++ b/tests/Serializer/PayloadSerializerTest.php @@ -422,7 +422,8 @@ public static function serializeAsEnvelopeDataProvider(): iterable {"event_id":"fc9442f5aef34234bb22b9a615e30ccd","sent_at":"2020-08-18T22:47:15Z","dsn":"http:\/\/public@example.com\/sentry\/1","sdk":{"name":"sentry.php","version":"4.11.1","packages":[{"name":"composer:sentry\/sentry","version":"4.11.1"}]}} {"type":"log","item_count":1,"content_type":"application\/vnd.sentry.items.log+json"} {"items":[{"timestamp":1597790835,"trace_id":"21160e9b836d479f81611368b2aa3d2c","level":"info","body":"A log message","attributes":[]}]} -TEXT, +TEXT + , ]; $event = Event::createEvent(new EventId('fc9442f5aef34234bb22b9a615e30ccd')); @@ -438,7 +439,8 @@ public static function serializeAsEnvelopeDataProvider(): iterable {"timestamp":1597790835,"platform":"php","sdk":{"name":"sentry.php","version":"$sdkVersion","packages":[{"name":"composer:sentry\/sentry","version":"$sdkVersion"}]}} {"type":"log","item_count":1,"content_type":"application\/vnd.sentry.items.log+json"} {"items":[{"timestamp":1597790835,"trace_id":"21160e9b836d479f81611368b2aa3d2c","level":"info","body":"A log message","attributes":[]}]} -TEXT, +TEXT + , ]; } } From 7c1a6b6a85583f50e4c60a656b87681c9ecd2be1 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 22:03:56 +0200 Subject: [PATCH 29/45] Prefix logs logs --- src/Logs/LogsAggregator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index d7d233c0d..4e7129dd4 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -105,7 +105,7 @@ public function add( // We check if it's a `LogsLogger` to avoid a infinite loop where the logger is logging the logs it's writing if ($logger !== null && !$logger instanceof LogsLogger) { - $logger->log((string) $log->getLevel(), $log->getBody(), $log->attributes()->toSimpleArray()); + $logger->log((string) $log->getLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray()); } $this->logs[] = $log; From 1c47d1cfcf81880f29a7d4ed50023f67ee9cf91c Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 15 May 2025 22:04:16 +0200 Subject: [PATCH 30/45] Append `$context` in debug loggers to output --- src/Logger/DebugFileLogger.php | 13 +++---------- src/Logger/DebugLogger.php | 26 ++++++++++++++++++++++++++ src/Logger/DebugStdOutLogger.php | 13 +++---------- 3 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 src/Logger/DebugLogger.php diff --git a/src/Logger/DebugFileLogger.php b/src/Logger/DebugFileLogger.php index 4875baa75..e096b4bba 100644 --- a/src/Logger/DebugFileLogger.php +++ b/src/Logger/DebugFileLogger.php @@ -4,9 +4,7 @@ namespace Sentry\Logger; -use Psr\Log\AbstractLogger; - -class DebugFileLogger extends AbstractLogger +class DebugFileLogger extends DebugLogger { /** * @var string @@ -18,13 +16,8 @@ public function __construct(string $filePath) $this->filePath = $filePath; } - /** - * @param mixed $level - * @param string|\Stringable $message - * @param mixed[] $context - */ - public function log($level, $message, array $context = []): void + public function write(string $message): void { - file_put_contents($this->filePath, \sprintf("sentry/sentry: [%s] %s\n", $level, (string) $message), \FILE_APPEND); + file_put_contents($this->filePath, $message, \FILE_APPEND); } } diff --git a/src/Logger/DebugLogger.php b/src/Logger/DebugLogger.php new file mode 100644 index 000000000..7ebe02115 --- /dev/null +++ b/src/Logger/DebugLogger.php @@ -0,0 +1,26 @@ +write( + \sprintf("sentry/sentry: [%s] %s\n", $level, $formattedMessageAndContext) + ); + } + + abstract public function write(string $message): void; +} diff --git a/src/Logger/DebugStdOutLogger.php b/src/Logger/DebugStdOutLogger.php index 5b2da8faf..8e31845d2 100644 --- a/src/Logger/DebugStdOutLogger.php +++ b/src/Logger/DebugStdOutLogger.php @@ -4,17 +4,10 @@ namespace Sentry\Logger; -use Psr\Log\AbstractLogger; - -class DebugStdOutLogger extends AbstractLogger +class DebugStdOutLogger extends DebugLogger { - /** - * @param mixed $level - * @param string|\Stringable $message - * @param mixed[] $context - */ - public function log($level, $message, array $context = []): void + public function write(string $message): void { - file_put_contents('php://stdout', \sprintf("sentry/sentry: [%s] %s\n", $level, (string) $message)); + file_put_contents('php://stdout', $message); } } From 5f6ddb65c4a7b7b9b1c5badb501041ad59ac8a82 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 20 May 2025 10:59:45 +0200 Subject: [PATCH 31/45] phpstan --- phpstan-baseline.neon | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 99fe2e6d6..3d18a0ef8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -53,12 +53,7 @@ parameters: - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" count: 1 - path: src/Logger/DebugFileLogger.php - - - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" - count: 1 - path: src/Logger/DebugStdOutLogger.php + path: src/Logger/DebugLogger.php - message: "#^Parameter \\#1 \\$level of method Monolog\\\\Handler\\\\AbstractHandler\\:\\:__construct\\(\\) expects 100\\|200\\|250\\|300\\|400\\|500\\|550\\|600\\|'ALERT'\\|'alert'\\|'CRITICAL'\\|'critical'\\|'DEBUG'\\|'debug'\\|'EMERGENCY'\\|'emergency'\\|'ERROR'\\|'error'\\|'INFO'\\|'info'\\|'NOTICE'\\|'notice'\\|'WARNING'\\|'warning'\\|Monolog\\\\Level, int\\|Monolog\\\\Level\\|string given\\.$#" From 1578fa5a41098f20dd5446b9bddd0c1d58be9dea Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 20 May 2025 11:09:55 +0200 Subject: [PATCH 32/45] Flatten attributes passed for logs --- src/Logger/LogsLogger.php | 5 --- src/Logs/Log.php | 6 ++- src/Logs/Logs.php | 40 +++++++++---------- src/Logs/LogsAggregator.php | 3 ++ src/Util/Arr.php | 44 +++++++++++++++++++++ tests/Logs/LogTest.php | 25 ++++++------ tests/Logs/LogsTest.php | 23 +++++++++++ tests/Util/ArrTest.php | 78 +++++++++++++++++++++++++++++++++++++ 8 files changed, 183 insertions(+), 41 deletions(-) create mode 100644 src/Util/Arr.php create mode 100644 tests/Util/ArrTest.php diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php index 7a4a7bd60..77ce3c824 100644 --- a/src/Logger/LogsLogger.php +++ b/src/Logger/LogsLogger.php @@ -24,23 +24,18 @@ public function log($level, $message, array $context = []): void switch ($level) { case 'emergency': case 'critical': - // @phpstan-ignore-next-line logger()->fatal((string) $message, [], $context); break; case 'error': - // @phpstan-ignore-next-line logger()->error((string) $message, [], $context); break; case 'warning': - // @phpstan-ignore-next-line logger()->warn((string) $message, [], $context); break; case 'debug': - // @phpstan-ignore-next-line logger()->debug((string) $message, [], $context); break; default: - // @phpstan-ignore-next-line logger()->info((string) $message, [], $context); break; } diff --git a/src/Logs/Log.php b/src/Logs/Log.php index 61732d69e..4e29885de 100644 --- a/src/Logs/Log.php +++ b/src/Logs/Log.php @@ -8,12 +8,14 @@ use Sentry\Attributes\AttributeBag; /** + * @phpstan-import-type AttributeSerialized from Attribute + * * @phpstan-type LogEnvelopeItem array{ * timestamp: int|float, * trace_id: string, * level: string, * body: string, - * attributes: array + * attributes: array * } */ class Log implements \JsonSerializable @@ -101,7 +103,7 @@ public function jsonSerialize(): array 'trace_id' => $this->traceId, 'level' => (string) $this->level, 'body' => $this->body, - 'attributes' => $this->attributes->all(), + 'attributes' => $this->attributes->toArray(), ]; } } diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index c1dc5ce1f..99bc34e43 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -4,12 +4,8 @@ namespace Sentry\Logs; -use Sentry\Attributes\Attribute; use Sentry\EventId; -/** - * @phpstan-import-type AttributeValue from Attribute - */ class Logs { /** @@ -37,9 +33,9 @@ public static function getInstance(): self } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function trace(string $message, array $values = [], array $attributes = []): void { @@ -47,9 +43,9 @@ public function trace(string $message, array $values = [], array $attributes = [ } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function debug(string $message, array $values = [], array $attributes = []): void { @@ -57,9 +53,9 @@ public function debug(string $message, array $values = [], array $attributes = [ } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function info(string $message, array $values = [], array $attributes = []): void { @@ -67,9 +63,9 @@ public function info(string $message, array $values = [], array $attributes = [] } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function warn(string $message, array $values = [], array $attributes = []): void { @@ -77,9 +73,9 @@ public function warn(string $message, array $values = [], array $attributes = [] } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function error(string $message, array $values = [], array $attributes = []): void { @@ -87,9 +83,9 @@ public function error(string $message, array $values = [], array $attributes = [ } /** - * @param string $message see sprintf for a description of format - * @param array $values see sprintf for a description of values - * @param array $attributes additional attributes to add to the log + * @param string $message see sprintf for a description of format + * @param array $values see sprintf for a description of values + * @param array $attributes additional attributes to add to the log */ public function fatal(string $message, array $values = [], array $attributes = []): void { diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 4e7129dd4..ce21fc00d 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -11,6 +11,7 @@ use Sentry\Logger\LogsLogger; use Sentry\SentrySdk; use Sentry\State\Scope; +use Sentry\Util\Arr; /** * @phpstan-import-type AttributeValue from Attribute @@ -76,6 +77,8 @@ public function add( $logger = $options->getLogger(); + $attributes = Arr::simpleDot($attributes); + foreach ($attributes as $key => $value) { $attribute = Attribute::tryFromValue($value); diff --git a/src/Util/Arr.php b/src/Util/Arr.php new file mode 100644 index 000000000..3a6df70cb --- /dev/null +++ b/src/Util/Arr.php @@ -0,0 +1,44 @@ + $array + * + * @return array + */ + public static function simpleDot(array $array): array + { + $results = []; + + $flatten = static function ($data, $prefix = '') use (&$results, &$flatten): void { + foreach ($data as $key => $value) { + $newKey = $prefix . $key; + + if (\is_array($value) && !empty($value) && !array_is_list($value)) { + $flatten($value, $newKey . '.'); + } else { + $results[$newKey] = $value; + } + } + }; + + $flatten($array); + + return $results; + } +} diff --git a/tests/Logs/LogTest.php b/tests/Logs/LogTest.php index 4dd5a223a..0b5a844c8 100644 --- a/tests/Logs/LogTest.php +++ b/tests/Logs/LogTest.php @@ -24,19 +24,20 @@ public function testJsonSerializesToExpected(): void $log->setAttribute('foo', 'bar'); $log->setAttribute('should-be-missing', ['foo' => 'bar']); - $serialized = json_decode((string) json_encode($log), true); - - $this->assertEquals([ - 'timestamp' => $timestamp, - 'trace_id' => '123', - 'level' => 'debug', - 'body' => 'foo', - 'attributes' => [ - 'foo' => [ - 'type' => 'string', - 'value' => 'bar', + $this->assertEquals( + [ + 'timestamp' => $timestamp, + 'trace_id' => '123', + 'level' => 'debug', + 'body' => 'foo', + 'attributes' => [ + 'foo' => [ + 'type' => 'string', + 'value' => 'bar', + ], ], ], - ], $serialized); + $log->jsonSerialize() + ); } } diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index c1c14076e..cf90af7a4 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -91,6 +91,29 @@ public function testLogWithTemplate(): void $this->assertNotNull(logger()->flush()); } + public function testLogWithNestedAttributes(): void + { + $this->assertEvent(function (Event $event) { + $this->assertCount(1, $event->getLogs()); + + $logItem = $event->getLogs()[0]->jsonSerialize(); + + $this->assertArrayHasKey('nested.foo', $logItem['attributes']); + $this->assertArrayNotHasKey('nested.should-be-missing', $logItem['attributes']); + + $this->assertEquals('bar', $logItem['attributes']['nested.foo']['value']); + }); + + logger()->info('Some message', [], [ + 'nested' => [ + 'foo' => 'bar', + 'should-be-missing' => [1, 2, 3], + ], + ]); + + $this->assertNotNull(logger()->flush()); + } + /** * @param callable(Event): void $assert */ diff --git a/tests/Util/ArrTest.php b/tests/Util/ArrTest.php new file mode 100644 index 000000000..5219d5ea1 --- /dev/null +++ b/tests/Util/ArrTest.php @@ -0,0 +1,78 @@ +assertSame($expectedResult, Arr::simpleDot($value)); + } + + public static function simpleDotDataProvider(): \Generator + { + yield [ + [1, 2, 3], + [1, 2, 3], + ]; + + yield [ + [ + 'key' => 'value', + ], + [ + 'key' => 'value', + ], + ]; + + yield [ + [ + 'key' => [ + 'key2' => 'value', + ], + ], + [ + 'key.key2' => 'value', + ], + ]; + + yield [ + [ + 'key' => ['foo', 'bar'], + ], + [ + 'key' => ['foo', 'bar'], + ], + ]; + + yield [ + [ + 'key' => [ + 'key2' => ['foo', 'bar'], + ], + ], + [ + 'key.key2' => ['foo', 'bar'], + ], + ]; + + $someClass = new \stdClass(); + + yield [ + [ + 'key' => $someClass, + ], + [ + 'key' => $someClass, + ], + ]; + } +} From 9d42e0f31aa5be27f232c4646a317063204edb1a Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 20 May 2025 11:22:17 +0200 Subject: [PATCH 33/45] Polyfill `array_is_list` --- src/Util/Arr.php | 24 +++++++++++++++++++++++- tests/Util/ArrTest.php | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Util/Arr.php b/src/Util/Arr.php index 3a6df70cb..5715fab98 100644 --- a/src/Util/Arr.php +++ b/src/Util/Arr.php @@ -29,7 +29,7 @@ public static function simpleDot(array $array): array foreach ($data as $key => $value) { $newKey = $prefix . $key; - if (\is_array($value) && !empty($value) && !array_is_list($value)) { + if (\is_array($value) && !empty($value) && !self::isList($value)) { $flatten($value, $newKey . '.'); } else { $results[$newKey] = $value; @@ -41,4 +41,26 @@ public static function simpleDot(array $array): array return $results; } + + /** + * Checks whether a given array is a list. + * + * `array_is_list` is introduced in PHP 8.1, so we have a polyfill for it. + * + * @see https://www.php.net/manual/en/function.array-is-list.php#126794 + * + * @param array $array + */ + public static function isList(array $array): bool + { + $i = 0; + + foreach ($array as $k => $v) { + if ($k !== $i++) { + return false; + } + } + + return true; + } } diff --git a/tests/Util/ArrTest.php b/tests/Util/ArrTest.php index 5219d5ea1..4a90b4178 100644 --- a/tests/Util/ArrTest.php +++ b/tests/Util/ArrTest.php @@ -75,4 +75,27 @@ public static function simpleDotDataProvider(): \Generator ], ]; } + + /** + * @dataProvider isListDataProvider + */ + public function testIsList(array $value, bool $expectedResult): void + { + $this->assertSame($expectedResult, Arr::isList($value)); + } + + public static function isListDataProvider(): \Generator + { + yield [ + [1, 2, 3], + true, + ]; + + yield [ + [ + 'key' => 'value', + ], + false, + ]; + } } From 6befa35e9f0efe1cdcf9e98433dfa21d0497e5a3 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 21 May 2025 20:10:06 +0200 Subject: [PATCH 34/45] Try to get trace ID from span first --- src/Logs/LogsAggregator.php | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index ce21fc00d..088fc9cfb 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -10,6 +10,7 @@ use Sentry\EventId; use Sentry\Logger\LogsLogger; use Sentry\SentrySdk; +use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Util\Arr; @@ -46,20 +47,9 @@ public function add( return; } - $scope = null; - - // This we push and pop a scope to get access to it because there is no accessor for the scope - $hub->configureScope(function (Scope $hubScope) use (&$scope) { - $scope = $hubScope; - }); - - \assert($scope !== null, 'The scope comes from the hub and cannot be null at this point.'); - - $traceId = $scope->getPropagationContext()->getTraceId(); - $options = $client->getOptions(); - $log = (new Log($timestamp, (string) $traceId, $level, vsprintf($message, $values))) + $log = (new Log($timestamp, $this->getTraceId($hub), $level, vsprintf($message, $values))) ->setAttribute('sentry.release', $options->getRelease()) ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) ->setAttribute('sentry.server.address', $options->getServerName()) @@ -141,4 +131,24 @@ public function flushWithoutEvent(): array return $logs; } + + private function getTraceId(HubInterface $hub): string + { + $span = $hub->getSpan(); + + if ($span !== null) { + return (string) $span->getTraceId(); + } + + $scope = null; + + // This we push and pop a scope to get access to it because there is no accessor for the scope + $hub->configureScope(function (Scope $hubScope) use (&$scope) { + $scope = $hubScope; + }); + + \assert($scope !== null, 'The scope comes from the hub and cannot be null at this point.'); + + return (string) $scope->getPropagationContext()->getTraceId(); + } } From 1bdc502da11ac309c3396a5834b1bb28439eaaa1 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 21 May 2025 20:16:25 +0200 Subject: [PATCH 35/45] Simplify getting the trace ID --- src/Logs/LogsAggregator.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 088fc9cfb..41b44cf03 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -140,15 +140,12 @@ private function getTraceId(HubInterface $hub): string return (string) $span->getTraceId(); } - $scope = null; + $traceId = ''; - // This we push and pop a scope to get access to it because there is no accessor for the scope - $hub->configureScope(function (Scope $hubScope) use (&$scope) { - $scope = $hubScope; + $hub->configureScope(function (Scope $scope) use (&$traceId) { + $traceId = (string) $scope->getPropagationContext()->getTraceId(); }); - \assert($scope !== null, 'The scope comes from the hub and cannot be null at this point.'); - - return (string) $scope->getPropagationContext()->getTraceId(); + return $traceId; } } From ae308e8eae9650e527fec2a65a43605681c56f60 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 12:51:08 +0200 Subject: [PATCH 36/45] =?UTF-8?q?Log=20if=20we=20don=E2=80=99t=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logs/LogsAggregator.php | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 41b44cf03..1a184489b 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -42,12 +42,23 @@ public function add( $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); - // There is no need to continue if there is no client or if logs are disabled - if ($client === null || !$client->getOptions()->getEnableLogs()) { + // There is no need to continue if there is no client + if ($client === null) { return; } $options = $client->getOptions(); + $sdkLogger = $options->getLogger(); + + if (!$options->getEnableLogs()) { + if ($sdkLogger !== null) { + $sdkLogger->info( + 'Log will be discarded because "enable_logs" is "false".' + ); + } + + return; + } $log = (new Log($timestamp, $this->getTraceId($hub), $level, vsprintf($message, $values))) ->setAttribute('sentry.release', $options->getRelease()) @@ -65,16 +76,14 @@ public function add( $log->setAttribute("sentry.message.parameter.{$key}", $value); } - $logger = $options->getLogger(); - $attributes = Arr::simpleDot($attributes); foreach ($attributes as $key => $value) { $attribute = Attribute::tryFromValue($value); if ($attribute === null) { - if ($logger !== null) { - $logger->info( + if ($sdkLogger !== null) { + $sdkLogger->info( \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) ); } @@ -86,8 +95,8 @@ public function add( $log = ($options->getBeforeSendLogCallback())($log); if ($log === null) { - if ($logger !== null) { - $logger->info( + if ($sdkLogger !== null) { + $sdkLogger->info( 'Log will be discarded because the "before_send_log" callback returned "null".', ['log' => $log] ); @@ -97,8 +106,8 @@ public function add( } // We check if it's a `LogsLogger` to avoid a infinite loop where the logger is logging the logs it's writing - if ($logger !== null && !$logger instanceof LogsLogger) { - $logger->log((string) $log->getLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray()); + if ($sdkLogger !== null && !$sdkLogger instanceof LogsLogger) { + $sdkLogger->log((string) $log->getLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray()); } $this->logs[] = $log; From a12e912459ce3062289e536536b3938f078c7dea Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 12:51:37 +0200 Subject: [PATCH 37/45] Do not flush logs with other events --- src/Client.php | 5 ----- src/Logs/Logs.php | 10 ---------- src/Logs/LogsAggregator.php | 14 -------------- src/Serializer/PayloadSerializer.php | 4 ---- tests/Logs/LogsTest.php | 16 ---------------- 5 files changed, 49 deletions(-) diff --git a/src/Client.php b/src/Client.php index 76c1538da..bf82e3297 100644 --- a/src/Client.php +++ b/src/Client.php @@ -359,11 +359,6 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco return null; } - // When we sent an non-logs event to Sentry, also flush the logs in the same envelope to prevent needing to make multiple requests - if ($event->getType() !== EventType::logs() && !\count($event->getLogs())) { - $event->setLogs(Logs::getInstance()->aggregator()->flushWithoutEvent()); - } - return $event; } diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index 99bc34e43..36aa0dddd 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -99,14 +99,4 @@ public function flush(): ?EventId { return $this->aggregator->flush(); } - - /** - * Get the logs aggregator. - * - * @internal - */ - public function aggregator(): LogsAggregator - { - return $this->aggregator; - } } diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 1a184489b..0a7553ce0 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -127,20 +127,6 @@ public function flush(): ?EventId return $hub->captureEvent($event); } - /** - * @internal - * - * @return Log[] - */ - public function flushWithoutEvent(): array - { - $logs = $this->logs; - - $this->logs = []; - - return $logs; - } - private function getTraceId(HubInterface $hub): string { $span = $hub->getSpan(); diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index 64fc84358..4878cc767 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -75,10 +75,6 @@ public function serialize(Event $event): string break; } - if ($event->getType() !== EventType::logs() && \count($event->getLogs())) { - $items[] = LogsItem::toEnvelopeItem($event); - } - return \sprintf("%s\n%s", JSON::encode($envelopeHeader), implode("\n", array_filter($items))); } } diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index cf90af7a4..450f841b9 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -59,22 +59,6 @@ public function testLogSentWhenEnabled(): void $this->assertNotNull(logger()->flush()); } - public function testLogSentWithOtherEvents(): void - { - $client = $this->assertEvent(function (Event $event) { - $this->assertCount(1, $event->getLogs()); - - $logItem = $event->getLogs()[0]->jsonSerialize(); - - $this->assertEquals(LogLevel::info(), $logItem['level']); - $this->assertEquals('Some info message', $logItem['body']); - }); - - logger()->info('Some info message'); - - $this->assertNotNull($client->captureMessage('Some message')); - } - public function testLogWithTemplate(): void { $this->assertEvent(function (Event $event) { From d6860a000f289c7b160eb475d6d3a2d4fc2bc093 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:01:38 +0200 Subject: [PATCH 38/45] Add test --- tests/Logs/LogsTest.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 450f841b9..05931b1c2 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -98,6 +98,32 @@ public function testLogWithNestedAttributes(): void $this->assertNotNull(logger()->flush()); } + /** + * @dataProvider logLevelDataProvider + */ + public function testLoggerSetsCorrectLevel(LogLevel $level): void + { + $this->assertEvent(function (Event $event) use ($level) { + $this->assertCount(1, $event->getLogs()); + + $this->assertEquals($level, $event->getLogs()[0]->getLevel()); + }); + + logger()->{(string) $level}('Some message'); + + $this->assertNotNull(logger()->flush()); + } + + public static function logLevelDataProvider(): \Generator + { + yield [LogLevel::trace()]; + yield [LogLevel::debug()]; + yield [LogLevel::info()]; + yield [LogLevel::warn()]; + yield [LogLevel::error()]; + yield [LogLevel::fatal()]; + } + /** * @param callable(Event): void $assert */ From 654ee8c3936d80db5414c920c8804e119039d194 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:02:05 +0200 Subject: [PATCH 39/45] Remove invalid test case --- tests/Serializer/PayloadSerializerTest.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/Serializer/PayloadSerializerTest.php b/tests/Serializer/PayloadSerializerTest.php index e37bafe0c..8cf6d16ec 100644 --- a/tests/Serializer/PayloadSerializerTest.php +++ b/tests/Serializer/PayloadSerializerTest.php @@ -422,23 +422,6 @@ public static function serializeAsEnvelopeDataProvider(): iterable {"event_id":"fc9442f5aef34234bb22b9a615e30ccd","sent_at":"2020-08-18T22:47:15Z","dsn":"http:\/\/public@example.com\/sentry\/1","sdk":{"name":"sentry.php","version":"4.11.1","packages":[{"name":"composer:sentry\/sentry","version":"4.11.1"}]}} {"type":"log","item_count":1,"content_type":"application\/vnd.sentry.items.log+json"} {"items":[{"timestamp":1597790835,"trace_id":"21160e9b836d479f81611368b2aa3d2c","level":"info","body":"A log message","attributes":[]}]} -TEXT - , - ]; - - $event = Event::createEvent(new EventId('fc9442f5aef34234bb22b9a615e30ccd')); - $event->setLogs([ - new Log(ClockMock::microtime(true), '21160e9b836d479f81611368b2aa3d2c', LogLevel::info(), 'A log message'), - ]); - - yield [ - $event, - << Date: Thu, 22 May 2025 13:02:34 +0200 Subject: [PATCH 40/45] CS --- src/Client.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index bf82e3297..6a5a4a4fc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,7 +8,6 @@ use Psr\Log\NullLogger; use Sentry\Integration\IntegrationInterface; use Sentry\Integration\IntegrationRegistry; -use Sentry\Logs\Logs; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\State\Scope; From 3f91e302784eb3d381a78a8f463a358760f14749 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:03:45 +0200 Subject: [PATCH 41/45] Fix typo --- src/Util/Arr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Util/Arr.php b/src/Util/Arr.php index 5715fab98..8773b6c07 100644 --- a/src/Util/Arr.php +++ b/src/Util/Arr.php @@ -5,7 +5,7 @@ namespace Sentry\Util; /** - * This class provides some utility methods to work with array's. + * This class provides some utility methods to work with arrays. * * @internal */ From 0fc9daa7f4c5313868164cb5e7af468f5f20d85c Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:39:10 +0200 Subject: [PATCH 42/45] Remove unused logger class --- src/Logger/LogsLogger.php | 43 --------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 src/Logger/LogsLogger.php diff --git a/src/Logger/LogsLogger.php b/src/Logger/LogsLogger.php deleted file mode 100644 index 77ce3c824..000000000 --- a/src/Logger/LogsLogger.php +++ /dev/null @@ -1,43 +0,0 @@ -fatal((string) $message, [], $context); - break; - case 'error': - logger()->error((string) $message, [], $context); - break; - case 'warning': - logger()->warn((string) $message, [], $context); - break; - case 'debug': - logger()->debug((string) $message, [], $context); - break; - default: - logger()->info((string) $message, [], $context); - break; - } - } -} From f835832d277066511fab40a3cfd40afaa37714ff Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:44:45 +0200 Subject: [PATCH 43/45] Add getter for aggregator used in other SDKs --- src/Logs/Logs.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Logs/Logs.php b/src/Logs/Logs.php index 36aa0dddd..99bc34e43 100644 --- a/src/Logs/Logs.php +++ b/src/Logs/Logs.php @@ -99,4 +99,14 @@ public function flush(): ?EventId { return $this->aggregator->flush(); } + + /** + * Get the logs aggregator. + * + * @internal + */ + public function aggregator(): LogsAggregator + { + return $this->aggregator; + } } From bef3448e4da5436103f9e05539096e1e37c37f38 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:46:10 +0200 Subject: [PATCH 44/45] Remove type check on removed class --- src/Logs/LogsAggregator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 0a7553ce0..7fa9bc231 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -8,7 +8,6 @@ use Sentry\Client; use Sentry\Event; use Sentry\EventId; -use Sentry\Logger\LogsLogger; use Sentry\SentrySdk; use Sentry\State\HubInterface; use Sentry\State\Scope; @@ -106,7 +105,7 @@ public function add( } // We check if it's a `LogsLogger` to avoid a infinite loop where the logger is logging the logs it's writing - if ($sdkLogger !== null && !$sdkLogger instanceof LogsLogger) { + if ($sdkLogger !== null) { $sdkLogger->log((string) $log->getLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray()); } From 1be4b5d9d04e3f2c953d71e1473bdea93e877c9b Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 22 May 2025 13:49:21 +0200 Subject: [PATCH 45/45] CS --- src/Client.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 6a5a4a4fc..36ee6a1ce 100644 --- a/src/Client.php +++ b/src/Client.php @@ -354,8 +354,6 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco ), ['event' => $beforeSendCallback] ); - - return null; } return $event;