diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3ff509ab0..e4ce16ef5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -25,21 +25,11 @@ parameters: count: 1 path: src/Dsn.php - - - message: "#^Method Sentry\\\\Event\\:\\:getMetrics\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Event.php - - message: "#^Method Sentry\\\\Event\\:\\:getMetricsSummary\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Event.php - - - message: "#^Method Sentry\\\\Event\\:\\:setMetrics\\(\\) has parameter \\$metrics with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Event.php - - message: "#^Method Sentry\\\\Event\\:\\:setMetricsSummary\\(\\) has parameter \\$metricsSummary with no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/Event.php b/src/Event.php index 5244f945c..e2d2a8c0f 100644 --- a/src/Event.php +++ b/src/Event.php @@ -7,6 +7,7 @@ use Sentry\Context\OsContext; use Sentry\Context\RuntimeContext; use Sentry\Logs\Log; +use Sentry\Metrics\Types\Metric; use Sentry\Profiling\Profile; use Sentry\Tracing\Span; @@ -71,6 +72,11 @@ final class Event */ private $logs = []; + /** + * @var Metric[] + */ + private $metrics = []; + /** * @var string|null The name of the server (e.g. the host name) */ @@ -241,9 +247,6 @@ 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. - */ public static function createMetrics(?EventId $eventId = null): self { return new self($eventId, EventType::metrics()); @@ -446,18 +449,20 @@ public function setLogs(array $logs): self } /** - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @return Metric[] */ public function getMetrics(): array { - return []; + return $this->metrics; } /** - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @param Metric[] $metrics */ public function setMetrics(array $metrics): self { + $this->metrics = $metrics; + return $this; } diff --git a/src/EventType.php b/src/EventType.php index 3c2d13fb3..679f96633 100644 --- a/src/EventType.php +++ b/src/EventType.php @@ -47,12 +47,9 @@ 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. - */ public static function metrics(): self { - return self::getInstance('metrics'); + return self::getInstance('trace_metric'); } /** @@ -71,6 +68,17 @@ public static function cases(): array ]; } + public function requiresEventId(): bool + { + switch ($this) { + case self::metrics(): + case self::logs(): + return false; + default: + return true; + } + } + public function __toString(): string { return $this->value; diff --git a/src/Metrics/Metrics.php b/src/Metrics/Metrics.php index 6f0fa6764..936538e19 100644 --- a/src/Metrics/Metrics.php +++ b/src/Metrics/Metrics.php @@ -6,9 +6,15 @@ use Sentry\EventId; use Sentry\Tracing\SpanContext; +use Sentry\Unit; use function Sentry\trace; +class_alias(Unit::class, '\Sentry\Metrics\MetricsUnit'); + +/** + * @deprecated use TraceMetrics instead + */ class Metrics { /** @@ -28,12 +34,12 @@ public static function getInstance(): self /** * @param array $tags * - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @deprecated Use TraceMetrics::count() instead. To be removed in 5.x. */ public function increment( string $key, float $value, - ?MetricsUnit $unit = null, + ?Unit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 @@ -43,12 +49,12 @@ public function increment( /** * @param array $tags * - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @deprecated Use TraceMetrics::distribution() instead. Metrics API is a no-op and will be removed in 5.x. */ public function distribution( string $key, float $value, - ?MetricsUnit $unit = null, + ?Unit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 @@ -58,12 +64,12 @@ public function distribution( /** * @param array $tags * - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @deprecated Use TraceMetrics::gauge() instead. To be removed in 5.x. */ public function gauge( string $key, float $value, - ?MetricsUnit $unit = null, + ?Unit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 @@ -79,7 +85,7 @@ public function gauge( public function set( string $key, $value, - ?MetricsUnit $unit = null, + ?Unit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php new file mode 100644 index 000000000..fa0dfd5be --- /dev/null +++ b/src/Metrics/MetricsAggregator.php @@ -0,0 +1,140 @@ + + */ + private $metrics; + + public function __construct() + { + $this->metrics = new RingBuffer(self::METRICS_BUFFER_SIZE); + } + + private const METRIC_TYPES = [ + CounterMetric::TYPE => CounterMetric::class, + DistributionMetric::TYPE => DistributionMetric::class, + GaugeMetric::TYPE => GaugeMetric::class, + ]; + + /** + * @param int|float $value + * @param array $attributes + */ + public function add( + string $type, + string $name, + $value, + array $attributes, + ?Unit $unit + ): void { + $hub = SentrySdk::getCurrentHub(); + $client = $hub->getClient(); + + if ($client instanceof Client) { + $options = $client->getOptions(); + + if ($options->getEnableMetrics() === false) { + return; + } + + $defaultAttributes = [ + 'sentry.sdk.name' => $client->getSdkIdentifier(), + 'sentry.sdk.version' => $client->getSdkVersion(), + 'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + 'server.address' => $options->getServerName(), + ]; + + if ($options->shouldSendDefaultPii()) { + $hub->configureScope(function (Scope $scope) use (&$defaultAttributes) { + $user = $scope->getUser(); + if ($user !== null) { + if ($user->getId() !== null) { + $defaultAttributes['user.id'] = $user->getId(); + } + if ($user->getEmail() !== null) { + $defaultAttributes['user.email'] = $user->getEmail(); + } + if ($user->getUsername() !== null) { + $defaultAttributes['user.name'] = $user->getUsername(); + } + } + }); + } + + $release = $options->getRelease(); + if ($release !== null) { + $defaultAttributes['sentry.release'] = $release; + } + + $attributes += $defaultAttributes; + } + + $spanId = null; + $traceId = null; + + $span = $hub->getSpan(); + if ($span !== null) { + $spanId = $span->getSpanId(); + $traceId = $span->getTraceId(); + } else { + $hub->configureScope(function (Scope $scope) use (&$traceId, &$spanId) { + $propagationContext = $scope->getPropagationContext(); + $traceId = $propagationContext->getTraceId(); + $spanId = $propagationContext->getSpanId(); + }); + } + + $metricTypeClass = self::METRIC_TYPES[$type]; + /** @var Metric $metric */ + /** @phpstan-ignore-next-line */ + $metric = new $metricTypeClass($name, $value, $traceId, $spanId, $attributes, microtime(true), $unit); + + if ($client !== null) { + $beforeSendMetric = $client->getOptions()->getBeforeSendMetricCallback(); + $metric = $beforeSendMetric($metric); + if ($metric === null) { + return; + } + } + + $this->metrics->push($metric); + } + + public function flush(): ?EventId + { + if ($this->metrics->isEmpty()) { + return null; + } + + $hub = SentrySdk::getCurrentHub(); + $event = Event::createMetrics()->setMetrics($this->metrics->drain()); + + return $hub->captureEvent($event); + } +} diff --git a/src/Metrics/TraceMetrics.php b/src/Metrics/TraceMetrics.php new file mode 100644 index 000000000..a3ef4a0a0 --- /dev/null +++ b/src/Metrics/TraceMetrics.php @@ -0,0 +1,100 @@ +aggregator = new MetricsAggregator(); + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new TraceMetrics(); + } + + return self::$instance; + } + + /** + * @param int|float $value + * @param array $attributes + */ + public function count( + string $name, + $value, + array $attributes = [], + ?Unit $unit = null + ): void { + $this->aggregator->add( + CounterMetric::TYPE, + $name, + $value, + $attributes, + $unit + ); + } + + /** + * @param int|float $value + * @param array $attributes + */ + public function distribution( + string $name, + $value, + array $attributes = [], + ?Unit $unit = null + ): void { + $this->aggregator->add( + DistributionMetric::TYPE, + $name, + $value, + $attributes, + $unit + ); + } + + /** + * @param int|float $value + * @param array $attributes + */ + public function gauge( + string $name, + $value, + array $attributes = [], + ?Unit $unit = null + ): void { + $this->aggregator->add( + GaugeMetric::TYPE, + $name, + $value, + $attributes, + $unit + ); + } + + public function flush(): ?EventId + { + return $this->aggregator->flush(); + } +} diff --git a/src/Metrics/Types/CounterMetric.php b/src/Metrics/Types/CounterMetric.php new file mode 100644 index 000000000..a309e176d --- /dev/null +++ b/src/Metrics/Types/CounterMetric.php @@ -0,0 +1,64 @@ + $attributes + */ + public function __construct( + string $name, + $value, + TraceId $traceId, + SpanId $spanId, + array $attributes, + float $timestamp, + ?Unit $unit + ) { + parent::__construct($name, $traceId, $spanId, $timestamp, $attributes, $unit); + + $this->value = (float) $value; + } + + /** + * @param int|float $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * @return int|float + */ + public function getValue() + { + return $this->value; + } + + public function getType(): string + { + return self::TYPE; + } +} diff --git a/src/Metrics/Types/DistributionMetric.php b/src/Metrics/Types/DistributionMetric.php new file mode 100644 index 000000000..bfbc85ce9 --- /dev/null +++ b/src/Metrics/Types/DistributionMetric.php @@ -0,0 +1,64 @@ + $attributes + */ + public function __construct( + string $name, + $value, + TraceId $traceId, + SpanId $spanId, + array $attributes, + float $timestamp, + ?Unit $unit + ) { + parent::__construct($name, $traceId, $spanId, $timestamp, $attributes, $unit); + + $this->value = (float) $value; + } + + /** + * @param int|float $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * @return int|float + */ + public function getValue() + { + return $this->value; + } + + public function getType(): string + { + return self::TYPE; + } +} diff --git a/src/Metrics/Types/GaugeMetric.php b/src/Metrics/Types/GaugeMetric.php new file mode 100644 index 000000000..2b58745be --- /dev/null +++ b/src/Metrics/Types/GaugeMetric.php @@ -0,0 +1,64 @@ + $attributes + */ + public function __construct( + string $name, + $value, + TraceId $traceId, + SpanId $spanId, + array $attributes, + float $timestamp, + ?Unit $unit + ) { + parent::__construct($name, $traceId, $spanId, $timestamp, $attributes, $unit); + + $this->value = (float) $value; + } + + /** + * @param int|float $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * @return int|float + */ + public function getValue() + { + return $this->value; + } + + public function getType(): string + { + return self::TYPE; + } +} diff --git a/src/Metrics/Types/Metric.php b/src/Metrics/Types/Metric.php new file mode 100644 index 000000000..999acbed9 --- /dev/null +++ b/src/Metrics/Types/Metric.php @@ -0,0 +1,111 @@ + $attributes + */ + public function __construct( + string $name, + TraceId $traceId, + SpanId $spanId, + float $timestamp, + array $attributes, + ?Unit $unit + ) { + $this->name = $name; + $this->unit = $unit; + $this->traceId = $traceId; + $this->spanId = $spanId; + $this->timestamp = $timestamp; + $this->attributes = new AttributeBag(); + + foreach ($attributes as $key => $value) { + $this->attributes->set($key, $value); + } + } + + /** + * @param int|float $value + */ + abstract public function setValue($value): void; + + abstract public function getType(): string; + + /** + * @return int|float + */ + abstract public function getValue(); + + public function getName(): string + { + return $this->name; + } + + public function getUnit(): ?Unit + { + return $this->unit; + } + + public function getTraceId(): TraceId + { + return $this->traceId; + } + + public function getSpanId(): SpanId + { + return $this->spanId; + } + + public function getAttributes(): AttributeBag + { + return $this->attributes; + } + + public function getTimestamp(): float + { + return $this->timestamp; + } +} diff --git a/src/Options.php b/src/Options.php index 84ead7836..8e1ee2279 100644 --- a/src/Options.php +++ b/src/Options.php @@ -10,6 +10,7 @@ use Sentry\Integration\ErrorListenerIntegration; use Sentry\Integration\IntegrationInterface; use Sentry\Logs\Log; +use Sentry\Metrics\Types\Metric; use Sentry\Transport\TransportInterface; use Symfony\Component\OptionsResolver\Options as SymfonyOptions; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -177,6 +178,31 @@ public function getEnableLogs(): bool return $this->options['enable_logs'] ?? false; } + /** + * Sets if metrics should be enabled or not. + */ + public function setEnableMetrics(bool $enableTracing): self + { + $options = array_merge($this->options, ['enable_metrics' => $enableTracing]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + + /** + * Returns whether metrics are enabled or not. + */ + public function getEnableMetrics(): bool + { + /** + * @var bool $enableMetrics + */ + $enableMetrics = $this->options['enable_metrics'] ?? true; + + return $enableMetrics; + } + /** * Sets the sampling factor to apply to transactions. A value of 0 will deny * sending any transactions, and a value of 1 will send 100% of transactions. @@ -676,6 +702,35 @@ public function getBeforeSendMetricsCallback(): callable return $this->options['before_send_metrics']; } + /** + * Gets a callback that will be invoked before a metric is added. + * Returning `null` means that the metric will be discarded. + */ + public function getBeforeSendMetricCallback(): callable + { + /** + * @var callable $callback + */ + $callback = $this->options['before_send_metric']; + + return $callback; + } + + /** + * Sets a new callback that is invoked before metrics are sent. + * Returning `null` means that the metric will be discarded. + * + * @return $this + */ + public function setBeforeSendMetricCallback(callable $callback): self + { + $options = array_merge($this->options, ['before_send_metric' => $callback]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + /** * Sets a callable to be called to decide whether metrics should * be send or not. @@ -1220,6 +1275,7 @@ private function configureOptions(OptionsResolver $resolver): void 'sample_rate' => 1, 'enable_tracing' => null, 'enable_logs' => false, + 'enable_metrics' => true, 'traces_sample_rate' => null, 'traces_sampler' => null, 'profiles_sample_rate' => null, @@ -1256,10 +1312,14 @@ private function configureOptions(OptionsResolver $resolver): void }, /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * Use `before_send_metric` instead. */ 'before_send_metrics' => static function (Event $metrics): ?Event { return null; }, + 'before_send_metric' => static function (Metric $metric): Metric { + return $metric; + }, 'trace_propagation_targets' => null, 'strict_trace_propagation' => false, 'tags' => [], @@ -1290,6 +1350,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('sample_rate', ['int', 'float']); $resolver->setAllowedTypes('enable_tracing', ['null', 'bool']); $resolver->setAllowedTypes('enable_logs', 'bool'); + $resolver->setAllowedTypes('enable_metrics', 'bool'); $resolver->setAllowedTypes('traces_sample_rate', ['null', 'int', 'float']); $resolver->setAllowedTypes('traces_sampler', ['null', 'callable']); $resolver->setAllowedTypes('profiles_sample_rate', ['null', 'int', 'float']); @@ -1309,6 +1370,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('before_send', ['callable']); $resolver->setAllowedTypes('before_send_transaction', ['callable']); $resolver->setAllowedTypes('before_send_log', 'callable'); + $resolver->setAllowedTypes('before_send_metric', ['callable']); $resolver->setAllowedTypes('ignore_exceptions', 'string[]'); $resolver->setAllowedTypes('ignore_transactions', 'string[]'); $resolver->setAllowedTypes('trace_propagation_targets', ['null', 'string[]']); diff --git a/src/Serializer/EnvelopItems/MetricsItem.php b/src/Serializer/EnvelopItems/MetricsItem.php new file mode 100644 index 000000000..1a2b41092 --- /dev/null +++ b/src/Serializer/EnvelopItems/MetricsItem.php @@ -0,0 +1,52 @@ +getMetrics(); + + $header = [ + 'type' => (string) EventType::metrics(), + 'item_count' => \count($metrics), + 'content_type' => 'application/vnd.sentry.items.trace-metric+json', + ]; + + return \sprintf( + "%s\n%s", + JSON::encode($header), + JSON::encode([ + 'items' => array_map(static function (Metric $metric): array { + return [ + 'timestamp' => $metric->getTimestamp(), + 'trace_id' => (string) $metric->getTraceId(), + 'span_id' => (string) $metric->getSpanId(), + 'name' => $metric->getName(), + 'value' => $metric->getValue(), + 'unit' => $metric->getUnit() ? (string) $metric->getUnit() : null, + 'type' => $metric->getType(), + 'attributes' => array_map(static function (Attribute $attribute): array { + return [ + 'type' => $attribute->getType(), + 'value' => $attribute->getValue(), + ]; + }, $metric->getAttributes()->all()), + ]; + }, $metrics), + ]) + ); + } +} diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index 4878cc767..45de0f29f 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -10,6 +10,7 @@ use Sentry\Serializer\EnvelopItems\CheckInItem; use Sentry\Serializer\EnvelopItems\EventItem; use Sentry\Serializer\EnvelopItems\LogsItem; +use Sentry\Serializer\EnvelopItems\MetricsItem; use Sentry\Serializer\EnvelopItems\ProfileItem; use Sentry\Serializer\EnvelopItems\TransactionItem; use Sentry\Tracing\DynamicSamplingContext; @@ -40,12 +41,15 @@ public function serialize(Event $event): string { // @see https://develop.sentry.dev/sdk/envelopes/#envelope-headers $envelopeHeader = [ - 'event_id' => (string) $event->getId(), 'sent_at' => gmdate('Y-m-d\TH:i:s\Z'), 'dsn' => (string) $this->options->getDsn(), 'sdk' => $event->getSdkPayload(), ]; + if ($event->getType()->requiresEventId()) { + $envelopeHeader['event_id'] = (string) $event->getId(); + } + $dynamicSamplingContext = $event->getSdkMetadata('dynamic_sampling_context'); if ($dynamicSamplingContext instanceof DynamicSamplingContext) { $entries = $dynamicSamplingContext->getEntries(); @@ -73,6 +77,9 @@ public function serialize(Event $event): string case EventType::logs(): $items[] = LogsItem::toEnvelopeItem($event); break; + case EventType::metrics(): + $items[] = MetricsItem::toEnvelopeItem($event); + break; } return \sprintf("%s\n%s", JSON::encode($envelopeHeader), implode("\n", array_filter($items))); diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index e55c4c948..09fe62a20 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -5,9 +5,9 @@ namespace Sentry\Tracing; use Sentry\EventId; -use Sentry\Metrics\MetricsUnit; use Sentry\SentrySdk; use Sentry\State\Scope; +use Sentry\Unit; /** * This class stores all the information about a span. @@ -548,7 +548,7 @@ public function setMetricsSummary( string $type, string $key, $value, - MetricsUnit $unit, + Unit $unit, array $tags ): void { } diff --git a/src/Metrics/MetricsUnit.php b/src/Unit.php similarity index 92% rename from src/Metrics/MetricsUnit.php rename to src/Unit.php index e94951fe3..5a83ab720 100644 --- a/src/Metrics/MetricsUnit.php +++ b/src/Unit.php @@ -2,12 +2,9 @@ declare(strict_types=1); -namespace Sentry\Metrics; +namespace Sentry; -/** - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. - */ -final class MetricsUnit implements \Stringable +final class Unit implements \Stringable { /** * @var string The value of the enum instance @@ -144,11 +141,17 @@ public static function percent(): self return self::getInstance('percent'); } + /** + * @deprecated `none` is not supported and will be removed in 5.x + */ public static function none(): self { return self::getInstance('none'); } + /** + * @deprecated custom unit types are currently not supported. Will be removed in 5.x + */ public static function custom(string $unit): self { return new self($unit); diff --git a/src/Util/RingBuffer.php b/src/Util/RingBuffer.php new file mode 100644 index 000000000..7852f53db --- /dev/null +++ b/src/Util/RingBuffer.php @@ -0,0 +1,215 @@ + + */ + private $buffer; + + /** + * @var int + */ + private $capacity; + + /** + * Points at the first element in the buffer. + * + * @var int + */ + private $head = 0; + + /** + * Points at the index where the next insertion will happen. + * If the buffer is not full, this will point to an empty array index. + * When full, it will point to the position where the oldest element is. + * + * @var int + */ + private $tail = 0; + + /** + * @var int + */ + private $count = 0; + + /** + * Creates a new buffer with a fixed capacity. + */ + public function __construct(int $capacity) + { + if ($capacity <= 0) { + throw new \RuntimeException('RingBuffer capacity must be greater than 0'); + } + $this->capacity = $capacity; + $this->buffer = new \SplFixedArray($capacity); + } + + /** + * Returns how many elements can be stored in the buffer before it starts overwriting + * old elements. + */ + public function capacity(): int + { + return $this->capacity; + } + + /** + * The current number of stored elements. + */ + public function count(): int + { + return $this->count; + } + + /** + * Whether the buffer contains any element or not. + */ + public function isEmpty(): bool + { + return $this->count === 0; + } + + /** + * Whether the buffer is at capacity and will start to overwrite old elements on push. + */ + public function isFull(): bool + { + return $this->count === $this->capacity; + } + + /** + * Adds a new element to the back of the buffer. If the buffer is at capacity, it will + * overwrite the oldest element. + * + * Insertion order is still maintained. + * + * @param T $value + */ + public function push($value): void + { + $this->buffer[$this->tail] = $value; + + $this->tail = ($this->tail + 1) % $this->capacity; + + if ($this->isFull()) { + $this->head = ($this->head + 1) % $this->capacity; + } else { + ++$this->count; + } + } + + /** + * Returns and removes the first element in the buffer. + * If the buffer is empty, it will return null instead. + * + * @return T|null + */ + public function shift() + { + if ($this->isEmpty()) { + return null; + } + $value = $this->buffer[$this->head]; + + $this->buffer[$this->head] = null; + + $this->head = ($this->head + 1) % $this->capacity; + --$this->count; + + return $value; + } + + /** + * Returns the last element in the buffer without removing it. + * If the buffer is empty, it will return null instead. + * + * @return T|null + */ + public function peekBack() + { + if ($this->isEmpty()) { + return null; + } + $idx = ($this->tail - 1 + $this->capacity) % $this->capacity; + + return $this->buffer[$idx]; + } + + /** + * Returns the first element in the buffer without removing it. + * If the buffer is empty, it will return null instead. + * + * @return T|null + */ + public function peekFront() + { + if ($this->isEmpty()) { + return null; + } + + return $this->buffer[$this->head]; + } + + /** + * Resets the count and removes all elements from the buffer. + */ + public function clear(): void + { + for ($i = 0; $i < $this->count; ++$i) { + $this->buffer[($this->head + $i) % $this->capacity] = null; + } + $this->count = 0; + $this->head = 0; + $this->tail = 0; + } + + /** + * Returns the content of the buffer as array. The resulting array will have the size of `count` + * and not `capacity`. + * + * @return array + */ + public function toArray(): array + { + $result = []; + for ($i = 0; $i < $this->count; ++$i) { + $value = $this->buffer[($this->head + $i) % $this->capacity]; + /** @var T $value */ + $result[] = $value; + } + + return $result; + } + + /** + * Returns the content of the buffer and clears all elements that it contains in the process. + * + * @return array + */ + public function drain(): array + { + $result = $this->toArray(); + $this->clear(); + + return $result; + } +} diff --git a/src/functions.php b/src/functions.php index c01349821..af967c8f9 100644 --- a/src/functions.php +++ b/src/functions.php @@ -9,6 +9,7 @@ use Sentry\Integration\IntegrationInterface; use Sentry\Logs\Logs; use Sentry\Metrics\Metrics; +use Sentry\Metrics\TraceMetrics; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; @@ -373,13 +374,18 @@ function logger(): Logs } /** - * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. + * @deprecated use `trace_metrics` instead */ function metrics(): Metrics { return Metrics::getInstance(); } +function trace_metrics(): TraceMetrics +{ + return TraceMetrics::getInstance(); +} + /** * Adds a feature flag evaluation to the current scope. * When invoked repeatedly for the same name, the most recent value is used. diff --git a/tests/Metrics/MetricsTest.php b/tests/Metrics/MetricsTest.php index 1e33a53d5..3de480657 100644 --- a/tests/Metrics/MetricsTest.php +++ b/tests/Metrics/MetricsTest.php @@ -104,12 +104,12 @@ public function testTiming(): void /** @var ClientInterface&MockObject $client */ $client = $this->createMock(ClientInterface::class); $client->expects($this->any()) - ->method('getOptions') - ->willReturn(new Options([ - 'release' => '1.0.0', - 'environment' => 'development', - 'attach_metric_code_locations' => true, - ])); + ->method('getOptions') + ->willReturn(new Options([ + 'release' => '1.0.0', + 'environment' => 'development', + 'attach_metric_code_locations' => true, + ])); $self = $this; diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php new file mode 100644 index 000000000..39ebf0705 --- /dev/null +++ b/tests/Metrics/TraceMetricsTest.php @@ -0,0 +1,119 @@ +bindClient(new Client(new Options(), StubTransport::getInstance())); + StubTransport::$events = []; + } + + public function testCounterMetrics(): void + { + trace_metrics()->count('test-count', 2, ['foo' => 'bar']); + trace_metrics()->count('test-count', 2, ['foo' => 'bar']); + trace_metrics()->flush(); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + $this->assertCount(2, $event->getMetrics()); + $metrics = $event->getMetrics(); + $metric = $metrics[0]; + $this->assertEquals('test-count', $metric->getName()); + $this->assertEquals(CounterMetric::TYPE, $metric->getType()); + $this->assertEquals(2, $metric->getValue()); + $this->assertArrayHasKey('foo', $metric->getAttributes()->toSimpleArray()); + } + + public function testGaugeMetrics(): void + { + trace_metrics()->gauge('test-gauge', 10, ['foo' => 'bar']); + trace_metrics()->flush(); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + $this->assertCount(1, $event->getMetrics()); + $metrics = $event->getMetrics(); + $metric = $metrics[0]; + $this->assertEquals('test-gauge', $metric->getName()); + $this->assertEquals(GaugeMetric::TYPE, $metric->getType()); + $this->assertEquals(10, $metric->getValue()); + $this->assertArrayHasKey('foo', $metric->getAttributes()->toSimpleArray()); + } + + public function testDistributionMetrics(): void + { + trace_metrics()->distribution('test-distribution', 10, ['foo' => 'bar']); + trace_metrics()->flush(); + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + $this->assertCount(1, $event->getMetrics()); + $metrics = $event->getMetrics(); + $metric = $metrics[0]; + $this->assertEquals('test-distribution', $metric->getName()); + $this->assertEquals(DistributionMetric::TYPE, $metric->getType()); + $this->assertEquals(10, $metric->getValue()); + $this->assertArrayHasKey('foo', $metric->getAttributes()->toSimpleArray()); + } + + public function testMetricsBufferFull(): void + { + for ($i = 0; $i < MetricsAggregator::METRICS_BUFFER_SIZE + 100; ++$i) { + trace_metrics()->count('test', 1, ['foo' => 'bar']); + } + trace_metrics()->flush(); + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + $metrics = $event->getMetrics(); + $this->assertCount(MetricsAggregator::METRICS_BUFFER_SIZE, $metrics); + } + + public function testEnableMetrics(): void + { + HubAdapter::getInstance()->bindClient(new Client(new Options([ + 'enable_metrics' => false, + ]), StubTransport::getInstance())); + + trace_metrics()->count('test-count', 2, ['foo' => 'bar']); + trace_metrics()->flush(); + + $this->assertEmpty(StubTransport::$events); + } + + public function testBeforeSendMetricAltersContent() + { + HubAdapter::getInstance()->bindClient(new Client(new Options([ + 'before_send_metric' => static function (Metric $metric) { + $metric->setValue(99999); + + return $metric; + }, + ]), StubTransport::getInstance())); + + trace_metrics()->count('test-count', 2, ['foo' => 'bar']); + trace_metrics()->flush(); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + $this->assertCount(1, $event->getMetrics()); + $metric = $event->getMetrics()[0]; + $this->assertEquals(99999, $metric->getValue()); + } +} diff --git a/tests/Serializer/PayloadSerializerTest.php b/tests/Serializer/PayloadSerializerTest.php index 1855bf487..3d3cc4c75 100644 --- a/tests/Serializer/PayloadSerializerTest.php +++ b/tests/Serializer/PayloadSerializerTest.php @@ -18,6 +18,9 @@ use Sentry\Frame; use Sentry\Logs\Log; use Sentry\Logs\LogLevel; +use Sentry\Metrics\Types\CounterMetric; +use Sentry\Metrics\Types\DistributionMetric; +use Sentry\Metrics\Types\GaugeMetric; use Sentry\MonitorConfig; use Sentry\MonitorSchedule; use Sentry\Options; @@ -31,6 +34,7 @@ use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TraceId; use Sentry\Tracing\TransactionMetadata; +use Sentry\Unit; use Sentry\UserDataBag; use Sentry\Util\ClockMock; use Sentry\Util\SentryUid; @@ -65,7 +69,7 @@ public static function serializeAsEnvelopeDataProvider(): iterable yield [ Event::createEvent(new EventId('fc9442f5aef34234bb22b9a615e30ccd')), <<\/","server_name":"foo.example.com","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","fingerprint":["myrpc","POST","\/foo.bar"],"modules":{"my.module.name":"1.0"},"extra":{"my_key":1,"some_other_value":"foo bar"},"tags":{"ios_version":"4.0","context":"production"},"user":{"id":"unique_id","username":"my_user","email":"foo@example.com","ip_address":"127.0.0.1","segment":"my_segment"},"contexts":{"os":{"name":"Linux","version":"4.19.104-microsoft-standard","build":"#1 SMP Wed Feb 19 06:37:35 UTC 2020","kernel_version":"Linux 7944782cd697 4.19.104-microsoft-standard #1 SMP Wed Feb 19 06:37:35 UTC 2020 x86_64"},"runtime":{"name":"php","sapi":"cli","version":"7.4.3"},"electron":{"type":"runtime","name":"Electron","version":"4.0"}},"breadcrumbs":{"values":[{"type":"user","category":"log","level":"info","timestamp":1597790835},{"type":"navigation","category":"log","level":"info","timestamp":1597790835,"data":{"from":"\/login","to":"\/dashboard"}},{"type":"default","category":"log","level":"info","timestamp":1597790835,"data":{"0":"foo","1":"bar"}}]},"request":{"method":"POST","url":"http:\/\/absolute.uri\/foo","query_string":"query=foobar&page=2","data":{"foo":"bar"},"cookies":{"PHPSESSID":"298zf09hf012fh2"},"headers":{"content-type":"text\/html"},"env":{"REMOTE_ADDR":"127.0.0.1"}},"exception":{"values":[{"type":"Exception","value":"chained exception","stacktrace":{"frames":[{"filename":"file\/name.py","lineno":3,"in_app":true},{"filename":"file\/name.py","lineno":3,"in_app":false,"abs_path":"absolute\/file\/name.py","function":"myfunction","raw_function":"raw_function_name","pre_context":["def foo():"," my_var = 'foo'"],"context_line":" raise ValueError()","post_context":["","def main():"],"vars":{"my_var":"value"}}]},"mechanism":{"type":"generic","handled":true,"data":{"code":123}}},{"type":"Exception","value":"initial exception"}]}} TEXT @@ -183,7 +187,7 @@ public static function serializeAsEnvelopeDataProvider(): iterable yield [ $event, <<setMetrics([ + new CounterMetric('test-counter', 5, new TraceId('21160e9b836d479f81611368b2aa3d2c'), new SpanId('d051f34163cd45fb'), ['foo' => 'bar'], 1597790835.0, Unit::bit()), + ]); + + yield [ + $event, + <<setMetrics([ + new GaugeMetric('test-gauge', 5, new TraceId('21160e9b836d479f81611368b2aa3d2c'), new SpanId('d051f34163cd45fb'), ['foo' => 'bar'], ClockMock::microtime(true), Unit::second()), + ]); + + yield [ + $event, + <<setMetrics([ + new DistributionMetric('test-distribution', 5, new TraceId('21160e9b836d479f81611368b2aa3d2c'), new SpanId('d051f34163cd45fb'), ['foo' => 'bar'], ClockMock::microtime(true), Unit::day()), + ]); + + yield [ + $event, + <<push('foo'); + $buffer->push('bar'); + + $result = $buffer->toArray(); + $this->assertSame(2, $buffer->count()); + $this->assertEquals(['foo', 'bar'], $result); + } + + public function testPeekBack(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertSame(2, $buffer->count()); + $this->assertSame('bar', $buffer->peekBack()); + } + + public function testPeekBackEmpty(): void + { + $buffer = new RingBuffer(5); + + $this->assertEmpty($buffer); + $this->assertNull($buffer->peekBack()); + } + + public function testPeekFront(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertSame(2, $buffer->count()); + $this->assertSame('foo', $buffer->peekFront()); + } + + public function testPeekFrontEmpty(): void + { + $buffer = new RingBuffer(5); + + $this->assertEmpty($buffer); + $this->assertNull($buffer->peekFront()); + } + + public function testFixedCapacity(): void + { + $buffer = new RingBuffer(2); + $buffer->push('foo'); + $buffer->push('bar'); + $buffer->push('baz'); + + $this->assertSame(2, $buffer->count()); + $this->assertEquals(['bar', 'baz'], $buffer->toArray()); + } + + public function testClear(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertSame(2, $buffer->count()); + $buffer->clear(); + $this->assertTrue($buffer->isEmpty()); + } + + public function testDrain(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertSame(2, $buffer->count()); + $result = $buffer->drain(); + $this->assertTrue($buffer->isEmpty()); + $this->assertEquals(['foo', 'bar'], $result); + } + + public function testShift(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertEquals('foo', $buffer->shift()); + $this->assertCount(1, $buffer); + } + + public function testShiftAndPush(): void + { + $buffer = new RingBuffer(5); + $buffer->push('foo'); + $buffer->push('bar'); + + $buffer->shift(); + + $buffer->push('baz'); + + $this->assertCount(2, $buffer); + $this->assertEquals(['bar', 'baz'], $buffer->toArray()); + } + + public function testCapacityOne(): void + { + $buffer = new RingBuffer(1); + $buffer->push('foo'); + $buffer->push('bar'); + + $this->assertCount(1, $buffer); + $this->assertSame('bar', $buffer->shift()); + } + + public function testInvalidCapacity(): void + { + $this->expectException(\RuntimeException::class); + $buffer = new RingBuffer(-1); + } + + public function testIsEmpty(): void + { + $buffer = new RingBuffer(5); + $this->assertTrue($buffer->isEmpty()); + } + + public function testIsFull(): void + { + $buffer = new RingBuffer(2); + $buffer->push('foo'); + $buffer->push('bar'); + $this->assertTrue($buffer->isFull()); + } +}