diff --git a/src/Event.php b/src/Event.php index 1ac6cc6af..b92b81e13 100644 --- a/src/Event.php +++ b/src/Event.php @@ -9,6 +9,7 @@ use Sentry\Logs\Log; use Sentry\Profiling\Profile; use Sentry\Tracing\Span; +use Sentry\Tracing\Spans\Span as SpanFirst; /** * This is the base class for classes containing event data. @@ -61,6 +62,11 @@ final class Event */ private $transaction; + /** + * @var Span[] The array of spans if it's a transaction + */ + private $spans = []; + /** * @var CheckIn|null The check in data */ @@ -151,11 +157,6 @@ final class Event */ private $breadcrumbs = []; - /** - * @var Span[] The array of spans if it's a transaction - */ - private $spans = []; - /** * @var ExceptionDataBag[] The exceptions */ @@ -231,6 +232,11 @@ public static function createTransaction(?EventId $eventId = null): self return new self($eventId, EventType::transaction()); } + public static function createSpans(?EventId $eventId = null): self + { + return new self($eventId, EventType::spans()); + } + public static function createCheckIn(?EventId $eventId = null): self { return new self($eventId, EventType::checkIn()); diff --git a/src/EventType.php b/src/EventType.php index 3c2d13fb3..cda1e54e3 100644 --- a/src/EventType.php +++ b/src/EventType.php @@ -37,6 +37,11 @@ public static function transaction(): self return self::getInstance('transaction'); } + public static function spans(): self + { + return self::getInstance('span'); + } + public static function checkIn(): self { return self::getInstance('check_in'); diff --git a/src/Profiling/Profile.php b/src/Profiling/Profile.php index 3e88e9cf2..27e26c921 100644 --- a/src/Profiling/Profile.php +++ b/src/Profiling/Profile.php @@ -249,6 +249,7 @@ public function getFormattedData(Event $event): ?array $samples[] = [ 'stack_id' => $stackId, 'thread_id' => self::THREAD_ID, + // @TODO(michi) timestamp microtime(true) 'elapsed_since_start_ns' => (int) round($duration * 1e+9), ]; } diff --git a/src/Serializer/EnvelopItems/SpansItem.php b/src/Serializer/EnvelopItems/SpansItem.php new file mode 100644 index 000000000..218434fb6 --- /dev/null +++ b/src/Serializer/EnvelopItems/SpansItem.php @@ -0,0 +1,33 @@ +getSpans(); + + $header = [ + 'type' => (string) $event->getType(), + 'item_count' => \count($spans), + 'content_type' => 'application/vnd.sentry.items.span.v2+json', + ]; + + return \sprintf( + "%s\n%s", + JSON::encode($header), + JSON::encode([ + 'items' => $spans, + ]) + ); + } +} diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index 4878cc767..4d7d8ece1 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -11,6 +11,7 @@ use Sentry\Serializer\EnvelopItems\EventItem; use Sentry\Serializer\EnvelopItems\LogsItem; use Sentry\Serializer\EnvelopItems\ProfileItem; +use Sentry\Serializer\EnvelopItems\SpansItem; use Sentry\Serializer\EnvelopItems\TransactionItem; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Util\JSON; @@ -67,6 +68,9 @@ public function serialize(Event $event): string $items[] = ProfileItem::toEnvelopeItem($event); } break; + case EventType::spans(): + $items[] = SpansItem::toEnvelopeItem($event); + break; case EventType::checkIn(): $items[] = CheckInItem::toEnvelopeItem($event); break; diff --git a/src/State/Hub.php b/src/State/Hub.php index bd51b440b..b7c7bd7af 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -348,7 +348,7 @@ public function getTransaction(): ?Transaction /** * {@inheritdoc} */ - public function setSpan(?Span $span): HubInterface + public function setSpan($span = null): HubInterface { $this->getScope()->setSpan($span); @@ -358,7 +358,7 @@ public function setSpan(?Span $span): HubInterface /** * {@inheritdoc} */ - public function getSpan(): ?Span + public function getSpan() { return $this->getScope()->getSpan(); } diff --git a/src/State/HubInterface.php b/src/State/HubInterface.php index 44d9e70e3..ced8695b6 100644 --- a/src/State/HubInterface.php +++ b/src/State/HubInterface.php @@ -146,10 +146,10 @@ public function getTransaction(): ?Transaction; /** * Returns the span that is on the Hub. */ - public function getSpan(): ?Span; + public function getSpan(); /** * Sets the span on the Hub. */ - public function setSpan(?Span $span): HubInterface; + public function setSpan($span = null): HubInterface; } diff --git a/src/State/Scope.php b/src/State/Scope.php index e4e054c3c..5e3af8cea 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -70,9 +70,6 @@ class Scope */ private $eventProcessors = []; - /** - * @var Span|null Set a Span on the Scope - */ private $span; /** @@ -386,10 +383,7 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op } // Apply the dynamic sampling context to errors if there is a Transaction on the Scope - $transaction = $this->span->getTransaction(); - if ($transaction !== null) { - $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); - } + $event->setSdkMetadata('dynamic_sampling_context', $this->span->metadata['dynamic_sampling_context'] ?? null); } else { if (!\array_key_exists('trace', $event->getContexts())) { $event->setContext('trace', $this->propagationContext->getTraceContext()); @@ -429,7 +423,7 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op /** * Returns the span that is on the scope. */ - public function getSpan(): ?Span + public function getSpan() { return $this->span; } @@ -441,7 +435,7 @@ public function getSpan(): ?Span * * @return $this */ - public function setSpan(?Span $span): self + public function setSpan($span = null): self { $this->span = $span; diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index 51308b06e..15d64cb8b 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -11,16 +11,8 @@ /** * This class stores all the information about a span. - * - * @phpstan-type MetricsSummary array{ - * min: int|float, - * max: int|float, - * sum: int|float, - * count: int, - * tags: array, - * } */ -class Span +class Span implements \JsonSerializable { /** * @var SpanId Span ID diff --git a/src/Tracing/SpanStatus.php b/src/Tracing/SpanStatus.php index 33c384552..2493302c0 100644 --- a/src/Tracing/SpanStatus.php +++ b/src/Tracing/SpanStatus.php @@ -128,6 +128,15 @@ public static function ok(): self return self::getInstance('ok'); } + /** + * Gets an instance of this enum representing the fact that the operation + * completed un-successfully. + */ + public static function error(): self + { + return self::getInstance('error'); + } + /** * Gets an instance of this enum representing the fact that the server returned * 4xx as response status code. diff --git a/src/Tracing/Spans/Span.php b/src/Tracing/Spans/Span.php new file mode 100644 index 000000000..14d5351f5 --- /dev/null +++ b/src/Tracing/Spans/Span.php @@ -0,0 +1,176 @@ +hub = SentrySdk::getCurrentHub(); + + $this->traceId = TraceId::generate(); + $this->spanId = SpanId::generate(); + + $this->attributes = new AttributeBag(); + } + + public static function make(): self + { + return new self(); + } + + public function start(): self + { + $this->startTimestamp = microtime(true); + + $parentSpan = $this->hub->getSpan(); + if ($parentSpan !== null) { + $this->parentSpanId = $parentSpan->spanId; + $this->traceId = $parentSpan->traceId; + } + + + $this->hub->setSpan($this); + + return $this; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function setStartTimestamp(float $startTime): self + { + $this->startTimestamp = $startTime; + + return $this; + } + + public function setEndTimestamp(float $endTime): self + { + $this->endTimestamp = $endTime; + + return $this; + } + + public function attributes(): AttributeBag + { + return $this->attributes; + } + + /** + * @param mixed $value + */ + public function setAttribute(string $key, $value): self + { + $this->attributes->set($key, $value); + + return $this; + } + + public function finish(): void + { + if ($this->endTimestamp !== null) { + // The span was already finished once + return; + } + + $this->endTimestamp = microtime(true); + $this->status = (string) SpanStatus::ok(); + + $parentSpan = $this->hub->getSpan(); + if ($parentSpan !== null) { + $this->hub->setSpan($parentSpan); + } + } + + public function getTraceContext(): array + { + $result = [ + 'span_id' => (string) $this->spanId, + 'trace_id' => (string) $this->traceId, + ]; + + if ($this->parentSpanId !== null) { + $result['parent_span_id'] = (string) $this->parentSpanId; + } + + // @TODO(michi) do we need all this data on the trace context? + // + // if ($this->description !== null) { + // $result['description'] = $this->description; + // } + + // if ($this->op !== null) { + // $result['op'] = $this->op; + // } + + // if ($this->status !== null) { + // $result['status'] = (string) $this->status; + // } + + // if (!empty($this->data)) { + // $result['data'] = $this->data; + // } + + // if (!empty($this->tags)) { + // $result['tags'] = $this->tags; + // } + + return $result; + } + + public function jsonSerialize(): array + { + return [ + 'trace_id' => (string) $this->traceId, + 'parent_span_id' => (string) $this->parentSpanId, + 'span_id' => (string) $this->spanId, + 'name' => $this->name, + 'status' => $this->status, + 'is_remote' => $this->parentSpanId ? true : false, + 'kind' => 'server', + 'start_timestamp' => $this->startTimestamp, + 'end_timestamp' => $this->endTimestamp, + 'attributes' => $this->attributes->toArray(), + ]; + } + } \ No newline at end of file diff --git a/src/Tracing/Spans/SpanId.php b/src/Tracing/Spans/SpanId.php new file mode 100644 index 000000000..51186ba46 --- /dev/null +++ b/src/Tracing/Spans/SpanId.php @@ -0,0 +1,55 @@ +value = $value; + } + + /** + * Generates a new span ID. + */ + public static function generate(): self + { + return new self(substr(SentryUid::generate(), 0, 16)); + } + + /** + * Compares whether two objects are equals. + * + * @param SpanId $other The object to compare + */ + public function isEqualTo(self $other): bool + { + return $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Tracing/Spans/Spans.php b/src/Tracing/Spans/Spans.php new file mode 100644 index 000000000..78a3bb8a0 --- /dev/null +++ b/src/Tracing/Spans/Spans.php @@ -0,0 +1,49 @@ +aggregator = new SpansAggregator(); + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function add(Span $span): void + { + $this->aggregator->add($span); + } + + public function flush(): ?EventId + { + return $this->aggregator->flush(); + } + + public function aggregator(): SpansAggregator + { + return $this->aggregator; + } +} diff --git a/src/Tracing/Spans/SpansAggregator.php b/src/Tracing/Spans/SpansAggregator.php new file mode 100644 index 000000000..953389b45 --- /dev/null +++ b/src/Tracing/Spans/SpansAggregator.php @@ -0,0 +1,40 @@ +spans[] = $span; + } + + public function flush(): ?EventId + { + if (empty($this->spans)) { + return null; + } + + $hub = SentrySdk::getCurrentHub(); + $event = Event::createSpans()->setSpans($this->spans); + + $this->spans = []; + + return $hub->captureEvent($event); + } +} diff --git a/src/Tracing/Spans/TraceId.php b/src/Tracing/Spans/TraceId.php new file mode 100644 index 000000000..54f47d6b7 --- /dev/null +++ b/src/Tracing/Spans/TraceId.php @@ -0,0 +1,45 @@ +value = $value; + } + + /** + * Generates a new trace ID. + */ + public static function generate(): self + { + return new self(SentryUid::generate()); + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/functions.php b/src/functions.php index 01705f631..778f13238 100644 --- a/src/functions.php +++ b/src/functions.php @@ -8,10 +8,12 @@ use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\IntegrationInterface; use Sentry\Logs\Logs; +use Sentry\Tracing\Spans; use Sentry\Metrics\Metrics; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; +use Sentry\Tracing\Spans\Spans as SpansFirst; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; @@ -373,6 +375,14 @@ function logger(): Logs return Logs::getInstance(); } +/** + * Get the Sentry Spans client. + */ +function tracing(): SpansFirst +{ + return SpansFirst::getInstance(); +} + /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */