From a0253ea2f60b34d6c75bb5df8b5d3078cccf384f Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 15 Apr 2024 18:25:45 +0200 Subject: [PATCH 1/9] WIP --- src/Event.php | 20 + src/EventType.php | 5 + src/Serializer/EnvelopItems/SpanItem.php | 86 ++++ src/Serializer/PayloadSerializer.php | 3 + src/Tracing/Spans/AbstractSpan.php | 600 +++++++++++++++++++++++ src/Tracing/Spans/SegmentId.php | 55 +++ src/Tracing/Spans/SegmentSpan.php | 13 + src/Tracing/Spans/Span.php | 88 ++++ src/Tracing/Spans/SpanId.php | 55 +++ src/Tracing/Spans/TraceId.php | 45 ++ 10 files changed, 970 insertions(+) create mode 100644 src/Serializer/EnvelopItems/SpanItem.php create mode 100644 src/Tracing/Spans/AbstractSpan.php create mode 100644 src/Tracing/Spans/SegmentId.php create mode 100644 src/Tracing/Spans/SegmentSpan.php create mode 100644 src/Tracing/Spans/Span.php create mode 100644 src/Tracing/Spans/SpanId.php create mode 100644 src/Tracing/Spans/TraceId.php diff --git a/src/Event.php b/src/Event.php index aded2808b..5ddf51522 100644 --- a/src/Event.php +++ b/src/Event.php @@ -9,6 +9,7 @@ use Sentry\Metrics\Types\AbstractType; use Sentry\Profiling\Profile; use Sentry\Tracing\Span; +use Sentry\Tracing\Spans\Span as OnlySpan; /** * This is the base class for classes containing event data. @@ -57,6 +58,8 @@ final class Event */ private $transaction; + private $span; + /** * @var CheckIn|null The check in data */ @@ -222,6 +225,11 @@ public static function createTransaction(?EventId $eventId = null): self return new self($eventId, EventType::transaction()); } + public static function createSpan(?EventId $eventId = null): self + { + return new self($eventId, EventType::span()); + } + public static function createCheckIn(?EventId $eventId = null): self { return new self($eventId, EventType::checkIn()); @@ -364,6 +372,18 @@ public function setTransaction(?string $transaction): self return $this; } + public function getSpan(): ?OnlySpan + { + return $this->span; + } + + public function setSpan(?OnlySpan $span): self + { + $this->span = $span; + + return $this; + } + public function getCheckIn(): ?CheckIn { return $this->checkIn; diff --git a/src/EventType.php b/src/EventType.php index 7e34cefc1..a25d3f473 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 span(): self + { + return self::getInstance('span'); + } + public static function checkIn(): self { return self::getInstance('check_in'); diff --git a/src/Serializer/EnvelopItems/SpanItem.php b/src/Serializer/EnvelopItems/SpanItem.php new file mode 100644 index 000000000..f9e8e2cf1 --- /dev/null +++ b/src/Serializer/EnvelopItems/SpanItem.php @@ -0,0 +1,86 @@ +, + * } + */ +class SpanItem implements EnvelopeItemInterface +{ + use BreadcrumbSeralizerTrait; + + public static function toEnvelopeItem(Event $event): string + { + $header = [ + 'type' => (string) $event->getType(), + 'content_type' => 'application/json', + ]; + + $payload = [ + 'platform' => 'php', + 'sdk' => [ + 'name' => $event->getSdkIdentifier(), + 'version' => $event->getSdkVersion(), + ], + ]; + + $span = $event->getSpan(); + + $payload['start_timestamp'] = $span->startTimestamp; + $payload['timestamp'] = $span->endTimestamp; + $payload['exclusive_time'] = $span->exclusiveTime; + + $payload['trace_id'] = (string) $span->traceId; + $payload['segment_id'] = (string) $span->segmentId; + $payload['span_id'] = (string) $span->spanId; + + $payload['is_segment'] = $span->isSegment; + + if ($span->description !== null) { + $payload['description'] = $span->description; + } + + if ($span->op !== null) { + $payload['op'] = $span->op; + } + + if ($span->status !== null) { + $payload['status'] = $span->status; + } + + if ($span->data !== null) { + $payload['data'] = $span->data; + } + + if ($event->getRelease() !== null) { + $payload['release'] = $event->getRelease(); + } + + if ($event->getEnvironment() !== null) { + $payload['environment'] = $event->getEnvironment(); + } + + // TBD: status + // TBD: transaction + // TBD: trace-origin + // TBD: profiling + + return sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); + } +} diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index e531a9a0e..4840e2227 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -11,6 +11,7 @@ use Sentry\Serializer\EnvelopItems\EventItem; use Sentry\Serializer\EnvelopItems\MetricsItem; use Sentry\Serializer\EnvelopItems\ProfileItem; +use Sentry\Serializer\EnvelopItems\SpanItem; use Sentry\Serializer\EnvelopItems\TransactionItem; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Util\JSON; @@ -75,6 +76,8 @@ public function serialize(Event $event): string } $items = $transactionItem; break; + case EventType::span(): + $items = SpanItem::toEnvelopeItem($event); case EventType::checkIn(): $items = CheckInItem::toEnvelopeItem($event); break; diff --git a/src/Tracing/Spans/AbstractSpan.php b/src/Tracing/Spans/AbstractSpan.php new file mode 100644 index 000000000..d2a32bda4 --- /dev/null +++ b/src/Tracing/Spans/AbstractSpan.php @@ -0,0 +1,600 @@ +, + * } + */ +abstract class AbstractSpan +{ + /** + * @var SpanId Span ID + */ + protected $spanId; + + /** + * @var TraceId Trace ID + */ + protected $traceId; + + /** + * @var string|null Description of the span + */ + protected $description; + + /** + * @var string|null Operation of the span + */ + protected $op; + + /** + * @var SpanStatus|null Completion status of the span + */ + protected $status; + + /** + * @var SpanId|null ID of the parent span + */ + protected $parentSpanId; + + /** + * @var bool|null Has the sample decision been made? + */ + protected $sampled; + + /** + * @var array A List of tags associated to this span + */ + protected $tags = []; + + /** + * @var array An arbitrary mapping of additional metadata + */ + protected $data = []; + + /** + * @var float Timestamp in seconds (epoch time) indicating when the span started + */ + protected $startTimestamp; + + /** + * @var float|null Timestamp in seconds (epoch time) indicating when the span ended + */ + protected $endTimestamp; + + /** + * @var SpanRecorder|null Reference instance to the {@see SpanRecorder} + */ + protected $spanRecorder; + + /** + * @var array> + */ + protected $metricsSummary = []; + + /** + * Constructor. + * + * @param SpanContext|null $context The context to create the span with + * + * @internal + */ + private function __construct() + { + // if ($context === null) { + // $this->traceId = TraceId::generate(); + // $this->spanId = SpanId::generate(); + // $this->startTimestamp = microtime(true); + + // return; + // } + + // $this->traceId = $context->getTraceId() ?? TraceId::generate(); + // $this->spanId = $context->getSpanId() ?? SpanId::generate(); + // $this->startTimestamp = $context->getStartTimestamp() ?? microtime(true); + // $this->parentSpanId = $context->getParentSpanId(); + // $this->description = $context->getDescription(); + // $this->op = $context->getOp(); + // $this->status = $context->getStatus(); + // $this->sampled = $context->getSampled(); + // $this->tags = $context->getTags(); + // $this->data = $context->getData(); + // $this->endTimestamp = $context->getEndTimestamp(); + } + + abstract static function make(): self; + + /** + * Sets the ID of the span. + * + * @param SpanId $spanId The ID + */ + public function setSpanId(SpanId $spanId): self + { + $this->spanId = $spanId; + + return $this; + } + + /** + * Gets the ID that determines which trace the span belongs to. + */ + public function getTraceId(): TraceId + { + return $this->traceId; + } + + /** + * Sets the ID that determines which trace the span belongs to. + * + * @param TraceId $traceId The ID + * + * @return $this + */ + public function setTraceId(TraceId $traceId) + { + $this->traceId = $traceId; + + return $this; + } + + /** + * Gets the ID that determines which span is the parent of the current one. + */ + public function getParentSpanId(): ?SpanId + { + return $this->parentSpanId; + } + + /** + * Sets the ID that determines which span is the parent of the current one. + * + * @param SpanId|null $parentSpanId The ID + * + * @return $this + */ + public function setParentSpanId(?SpanId $parentSpanId) + { + $this->parentSpanId = $parentSpanId; + + return $this; + } + + /** + * Gets the timestamp representing when the measuring started. + */ + public function getStartTimestamp(): float + { + return $this->startTimestamp; + } + + /** + * Sets the timestamp representing when the measuring started. + * + * @param float $startTimestamp The timestamp + * + * @return $this + */ + public function setStartTimestamp(float $startTimestamp) + { + $this->startTimestamp = $startTimestamp; + + return $this; + } + + /** + * Gets the timestamp representing when the measuring finished. + */ + public function getEndTimestamp(): ?float + { + return $this->endTimestamp; + } + + /** + * Gets a description of the span's operation, which uniquely identifies + * the span but is consistent across instances of the span. + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Sets a description of the span's operation, which uniquely identifies + * the span but is consistent across instances of the span. + * + * @param string|null $description The description + * + * @return $this + */ + public function setDescription(?string $description) + { + $this->description = $description; + + return $this; + } + + /** + * Gets a short code identifying the type of operation the span is measuring. + */ + public function getOp(): ?string + { + return $this->op; + } + + /** + * Sets a short code identifying the type of operation the span is measuring. + * + * @param string|null $op The short code + * + * @return $this + */ + public function setOp(?string $op) + { + $this->op = $op; + + return $this; + } + + /** + * Gets the status of the span/transaction. + */ + public function getStatus(): ?SpanStatus + { + return $this->status; + } + + /** + * Sets the status of the span/transaction. + * + * @param SpanStatus|null $status The status + * + * @return $this + */ + public function setStatus(?SpanStatus $status) + { + $this->status = $status; + + return $this; + } + + /** + * Sets the HTTP status code and the status of the span/transaction. + * + * @param int $statusCode The HTTP status code + * + * @return $this + */ + public function setHttpStatus(int $statusCode) + { + SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($statusCode) { + $scope->setContext('response', [ + 'status_code' => $statusCode, + ]); + }); + + $status = SpanStatus::createFromHttpStatusCode($statusCode); + + if ($status !== SpanStatus::unknownError()) { + $this->status = $status; + } + + return $this; + } + + /** + * Gets a map of tags for this event. + * + * @return array + */ + public function getTags(): array + { + return $this->tags; + } + + /** + * Sets a map of tags for this event. This method will merge the given tags with + * the existing ones. + * + * @param array $tags The tags + * + * @return $this + */ + public function setTags(array $tags) + { + $this->tags = array_merge($this->tags, $tags); + + return $this; + } + + /** + * Gets the ID of the span. + */ + public function getSpanId(): SpanId + { + return $this->spanId; + } + + /** + * Gets the flag determining whether this span should be sampled or not. + */ + public function getSampled(): ?bool + { + return $this->sampled; + } + + /** + * Sets the flag determining whether this span should be sampled or not. + * + * @param bool $sampled Whether to sample or not this span + * + * @return $this + */ + public function setSampled(?bool $sampled) + { + $this->sampled = $sampled; + + return $this; + } + + /** + * Gets a map of arbitrary data. + * + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * Sets a map of arbitrary data. This method will merge the given data with + * the existing one. + * + * @param array $data The data + * + * @return $this + */ + public function setData(array $data) + { + $this->data = array_merge($this->data, $data); + + return $this; + } + + /** + * Gets the data in a format suitable for storage in the "trace" context. + * + * @return array + * + * @psalm-return array{ + * data?: array, + * description?: string, + * op?: string, + * parent_span_id?: string, + * span_id: string, + * status?: string, + * tags?: array, + * trace_id: string + * } + */ + 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; + } + + 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; + } + + /** + * Sets the finish timestamp on the current span. + * + * @param float|null $endTimestamp Takes an endTimestamp if the end should not be the time when you call this function + * + * @return EventId|null Finish for a span always returns null + */ + public function finish(?float $endTimestamp = null): ?EventId + { + $this->endTimestamp = $endTimestamp ?? microtime(true); + + return null; + } + + /** + * Creates a new {@see Span} while setting the current ID as `parentSpanId`. + * Also the `sampled` decision will be inherited. + * + * @param SpanContext $context The context of the child span + */ + public function startChild(SpanContext $context): self + { + $context = clone $context; + $context->setSampled($this->sampled); + $context->setParentSpanId($this->spanId); + $context->setTraceId($this->traceId); + + $span = new self($context); + $span->transaction = $this->transaction; + $span->spanRecorder = $this->spanRecorder; + + if ($span->spanRecorder !== null) { + $span->spanRecorder->add($span); + } + + return $span; + } + + /** + * Gets the span recorder attached to this span. + * + * @internal + */ + public function getSpanRecorder(): ?SpanRecorder + { + return $this->spanRecorder; + } + + /** + * Detaches the span recorder from this instance. + * + * @return $this + */ + public function detachSpanRecorder() + { + $this->spanRecorder = null; + + return $this; + } + + /** + * @return array> + */ + public function getMetricsSummary(): array + { + return $this->metricsSummary; + } + + /** + * @param string|int|float $value + * @param string[] $tags + */ + public function setMetricsSummary( + string $type, + string $key, + $value, + MetricsUnit $unit, + array $tags + ): void { + $mri = sprintf('%s:%s@%s', $type, $key, (string) $unit); + $bucketKey = $mri . serialize($tags); + + if ( + isset($this->metricsSummary[$mri]) + && \array_key_exists($bucketKey, $this->metricsSummary[$mri]) + ) { + if ($type === SetType::TYPE) { + $value = 1.0; + } else { + $value = (float) $value; + } + + $summary = $this->metricsSummary[$mri][$bucketKey]; + $this->metricsSummary[$mri][$bucketKey] = [ + 'min' => min($summary['min'], $value), + 'max' => max($summary['max'], $value), + 'sum' => $summary['sum'] + $value, + 'count' => $summary['count'] + 1, + 'tags' => $tags, + ]; + } else { + if ($type === SetType::TYPE) { + $value = 0.0; + } else { + $value = (float) $value; + } + + $this->metricsSummary[$mri][$bucketKey] = [ + 'min' => $value, + 'max' => $value, + 'sum' => $value, + 'count' => 1, + 'tags' => $tags, + ]; + } + } + + /** + * Returns the transaction containing this span. + */ + public function getTransaction(): ?Transaction + { + return $this->transaction; + } + + /** + * Returns a string that can be used for the `sentry-trace` header & meta tag. + */ + public function toTraceparent(): string + { + $sampled = ''; + + if ($this->sampled !== null) { + $sampled = $this->sampled ? '-1' : '-0'; + } + + return sprintf('%s-%s%s', (string) $this->traceId, (string) $this->spanId, $sampled); + } + + /** + * Returns a string that can be used for the W3C `traceparent` header & meta tag. + */ + public function toW3CTraceparent(): string + { + $sampled = ''; + + if ($this->sampled !== null) { + $sampled = $this->sampled ? '01' : '00'; + } else { + // If no sampling decision was made, set the flag to 00 + $sampled = '00'; + } + + return sprintf('00-%s-%s-%s', (string) $this->traceId, (string) $this->spanId, $sampled); + } + + /** + * Returns a string that can be used for the `baggage` header & meta tag. + */ + public function toBaggage(): string + { + $transaction = $this->getTransaction(); + + if ($transaction !== null) { + return (string) $transaction->getDynamicSamplingContext(); + } + + return ''; + } +} diff --git a/src/Tracing/Spans/SegmentId.php b/src/Tracing/Spans/SegmentId.php new file mode 100644 index 000000000..3b33b2bfe --- /dev/null +++ b/src/Tracing/Spans/SegmentId.php @@ -0,0 +1,55 @@ +value = $value; + } + + /** + * Generates a new segment ID. + */ + public static function generate(): self + { + return new self(substr(SentryUid::generate(), 0, 16)); + } + + /** + * Compares whether two objects are equals. + * + * @param SegmentId $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/SegmentSpan.php b/src/Tracing/Spans/SegmentSpan.php new file mode 100644 index 000000000..4f85aa707 --- /dev/null +++ b/src/Tracing/Spans/SegmentSpan.php @@ -0,0 +1,13 @@ +hub = SentrySdk::getCurrentHub(); + + $this->traceId = TraceId::generate(); + $this->segmentId = SegmentId::generate(); + $this->spanId = SpanId::generate(); + + $this->isSegment = true; + } + + public static function make(): self + { + return new self(); + } + + public function start(): self + { + $this->startTimestamp = microtime(true); + + return $this; + } + + public function setOp(string $op): self + { + $this->op = $op; + + return $this; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + + public function finish(): ?EventId + { + $this->endTimestamp = microtime(true); + $this->exclusiveTime = $this->endTimestamp - $this->startTimestamp; + + $this->status = (string) SpanStatus::ok(); + + $event = Event::createSpan(); + $event->setSpan($this); + + return $this->hub->captureEvent($event); + } + } \ 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/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; + } +} From ae74397f18b1cb303c6452300675b173c094ca8b Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Tue, 16 Apr 2024 18:43:54 +0200 Subject: [PATCH 2/9] WIP --- src/Serializer/EnvelopItems/SpanItem.php | 51 +- src/Serializer/PayloadSerializer.php | 1 + src/State/Hub.php | 4 +- src/State/HubInterface.php | 4 +- src/State/Scope.php | 12 +- src/Tracing/Spans/AbstractSpan.php | 600 ----------------------- src/Tracing/Spans/SegmentSpan.php | 13 - src/Tracing/Spans/Span.php | 161 +++++- 8 files changed, 206 insertions(+), 640 deletions(-) delete mode 100644 src/Tracing/Spans/AbstractSpan.php delete mode 100644 src/Tracing/Spans/SegmentSpan.php diff --git a/src/Serializer/EnvelopItems/SpanItem.php b/src/Serializer/EnvelopItems/SpanItem.php index f9e8e2cf1..56eb0bd7e 100644 --- a/src/Serializer/EnvelopItems/SpanItem.php +++ b/src/Serializer/EnvelopItems/SpanItem.php @@ -52,6 +52,10 @@ public static function toEnvelopeItem(Event $event): string $payload['is_segment'] = $span->isSegment; + if ($span->parentSpanId !== null) { + $payload['parent_span_id'] = (string) $span->parentSpanId; + } + if ($span->description !== null) { $payload['description'] = $span->description; } @@ -64,10 +68,22 @@ public static function toEnvelopeItem(Event $event): string $payload['status'] = $span->status; } - if ($span->data !== null) { + if (!empty($span->data)) { $payload['data'] = $span->data; } + // if (!empty($span->tags)) { + // $payload['tags'] = $span->tags; + // } + + // if (!empty($span->context)) { + // $payload['context'] = $span->context; + // } + + // if (!empty($span->metricsSummary)) { + // $payload['_metrics_summary'] = self::serializeMetricsSummary($span->metricsSummary); + // } + if ($event->getRelease() !== null) { $payload['release'] = $event->getRelease(); } @@ -76,11 +92,38 @@ public static function toEnvelopeItem(Event $event): string $payload['environment'] = $event->getEnvironment(); } - // TBD: status - // TBD: transaction + // In general, mainly use data as it makes the SDK simpler + // See https://github.com/getsentry/rfcs/blob/main/text/0116-sentry-semantic-conventions.md + + // TBD: description -> name (OTel does the same) ✅ + // TBD: status -> use only three, HTTP status as context ✅ + // TBD: transaction -> confusing -> data.sentry.segment.name (data.sentry.segment_name) ✅ // TBD: trace-origin - // TBD: profiling + // TBD: profiling -> data.sentry.profiler_id + // TBD: tags?? -> this could become sentry.tags... field at one point + + // TBD: exclusive_time can't be calculated for single spans + // see https://sentry.my.sentry.io/organizations/sentry/issues/797887/events/?query=sdk%3A%22sentry.javascript.browser%2F7.109.0%22 + // we will remove the requirement for exclusive_time on Relay return sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } + + /** + * @param array> $metricsSummary + * + * @return array + */ + protected static function serializeMetricsSummary(array $metricsSummary): array + { + $formattedSummary = []; + + foreach ($metricsSummary as $mri => $metrics) { + foreach ($metrics as $metric) { + $formattedSummary[$mri][] = $metric; + } + } + + return $formattedSummary; + } } diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php index 4840e2227..7a2120435 100644 --- a/src/Serializer/PayloadSerializer.php +++ b/src/Serializer/PayloadSerializer.php @@ -78,6 +78,7 @@ public function serialize(Event $event): string break; case EventType::span(): $items = SpanItem::toEnvelopeItem($event); + break; case EventType::checkIn(): $items = CheckInItem::toEnvelopeItem($event); break; diff --git a/src/State/Hub.php b/src/State/Hub.php index a1c01a5e2..02384a5e2 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -342,7 +342,7 @@ public function getTransaction(): ?Transaction /** * {@inheritdoc} */ - public function setSpan(?Span $span): HubInterface + public function setSpan($span = null): HubInterface { $this->getScope()->setSpan($span); @@ -352,7 +352,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 546072f4e..da51a78b7 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/Spans/AbstractSpan.php b/src/Tracing/Spans/AbstractSpan.php deleted file mode 100644 index d2a32bda4..000000000 --- a/src/Tracing/Spans/AbstractSpan.php +++ /dev/null @@ -1,600 +0,0 @@ -, - * } - */ -abstract class AbstractSpan -{ - /** - * @var SpanId Span ID - */ - protected $spanId; - - /** - * @var TraceId Trace ID - */ - protected $traceId; - - /** - * @var string|null Description of the span - */ - protected $description; - - /** - * @var string|null Operation of the span - */ - protected $op; - - /** - * @var SpanStatus|null Completion status of the span - */ - protected $status; - - /** - * @var SpanId|null ID of the parent span - */ - protected $parentSpanId; - - /** - * @var bool|null Has the sample decision been made? - */ - protected $sampled; - - /** - * @var array A List of tags associated to this span - */ - protected $tags = []; - - /** - * @var array An arbitrary mapping of additional metadata - */ - protected $data = []; - - /** - * @var float Timestamp in seconds (epoch time) indicating when the span started - */ - protected $startTimestamp; - - /** - * @var float|null Timestamp in seconds (epoch time) indicating when the span ended - */ - protected $endTimestamp; - - /** - * @var SpanRecorder|null Reference instance to the {@see SpanRecorder} - */ - protected $spanRecorder; - - /** - * @var array> - */ - protected $metricsSummary = []; - - /** - * Constructor. - * - * @param SpanContext|null $context The context to create the span with - * - * @internal - */ - private function __construct() - { - // if ($context === null) { - // $this->traceId = TraceId::generate(); - // $this->spanId = SpanId::generate(); - // $this->startTimestamp = microtime(true); - - // return; - // } - - // $this->traceId = $context->getTraceId() ?? TraceId::generate(); - // $this->spanId = $context->getSpanId() ?? SpanId::generate(); - // $this->startTimestamp = $context->getStartTimestamp() ?? microtime(true); - // $this->parentSpanId = $context->getParentSpanId(); - // $this->description = $context->getDescription(); - // $this->op = $context->getOp(); - // $this->status = $context->getStatus(); - // $this->sampled = $context->getSampled(); - // $this->tags = $context->getTags(); - // $this->data = $context->getData(); - // $this->endTimestamp = $context->getEndTimestamp(); - } - - abstract static function make(): self; - - /** - * Sets the ID of the span. - * - * @param SpanId $spanId The ID - */ - public function setSpanId(SpanId $spanId): self - { - $this->spanId = $spanId; - - return $this; - } - - /** - * Gets the ID that determines which trace the span belongs to. - */ - public function getTraceId(): TraceId - { - return $this->traceId; - } - - /** - * Sets the ID that determines which trace the span belongs to. - * - * @param TraceId $traceId The ID - * - * @return $this - */ - public function setTraceId(TraceId $traceId) - { - $this->traceId = $traceId; - - return $this; - } - - /** - * Gets the ID that determines which span is the parent of the current one. - */ - public function getParentSpanId(): ?SpanId - { - return $this->parentSpanId; - } - - /** - * Sets the ID that determines which span is the parent of the current one. - * - * @param SpanId|null $parentSpanId The ID - * - * @return $this - */ - public function setParentSpanId(?SpanId $parentSpanId) - { - $this->parentSpanId = $parentSpanId; - - return $this; - } - - /** - * Gets the timestamp representing when the measuring started. - */ - public function getStartTimestamp(): float - { - return $this->startTimestamp; - } - - /** - * Sets the timestamp representing when the measuring started. - * - * @param float $startTimestamp The timestamp - * - * @return $this - */ - public function setStartTimestamp(float $startTimestamp) - { - $this->startTimestamp = $startTimestamp; - - return $this; - } - - /** - * Gets the timestamp representing when the measuring finished. - */ - public function getEndTimestamp(): ?float - { - return $this->endTimestamp; - } - - /** - * Gets a description of the span's operation, which uniquely identifies - * the span but is consistent across instances of the span. - */ - public function getDescription(): ?string - { - return $this->description; - } - - /** - * Sets a description of the span's operation, which uniquely identifies - * the span but is consistent across instances of the span. - * - * @param string|null $description The description - * - * @return $this - */ - public function setDescription(?string $description) - { - $this->description = $description; - - return $this; - } - - /** - * Gets a short code identifying the type of operation the span is measuring. - */ - public function getOp(): ?string - { - return $this->op; - } - - /** - * Sets a short code identifying the type of operation the span is measuring. - * - * @param string|null $op The short code - * - * @return $this - */ - public function setOp(?string $op) - { - $this->op = $op; - - return $this; - } - - /** - * Gets the status of the span/transaction. - */ - public function getStatus(): ?SpanStatus - { - return $this->status; - } - - /** - * Sets the status of the span/transaction. - * - * @param SpanStatus|null $status The status - * - * @return $this - */ - public function setStatus(?SpanStatus $status) - { - $this->status = $status; - - return $this; - } - - /** - * Sets the HTTP status code and the status of the span/transaction. - * - * @param int $statusCode The HTTP status code - * - * @return $this - */ - public function setHttpStatus(int $statusCode) - { - SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($statusCode) { - $scope->setContext('response', [ - 'status_code' => $statusCode, - ]); - }); - - $status = SpanStatus::createFromHttpStatusCode($statusCode); - - if ($status !== SpanStatus::unknownError()) { - $this->status = $status; - } - - return $this; - } - - /** - * Gets a map of tags for this event. - * - * @return array - */ - public function getTags(): array - { - return $this->tags; - } - - /** - * Sets a map of tags for this event. This method will merge the given tags with - * the existing ones. - * - * @param array $tags The tags - * - * @return $this - */ - public function setTags(array $tags) - { - $this->tags = array_merge($this->tags, $tags); - - return $this; - } - - /** - * Gets the ID of the span. - */ - public function getSpanId(): SpanId - { - return $this->spanId; - } - - /** - * Gets the flag determining whether this span should be sampled or not. - */ - public function getSampled(): ?bool - { - return $this->sampled; - } - - /** - * Sets the flag determining whether this span should be sampled or not. - * - * @param bool $sampled Whether to sample or not this span - * - * @return $this - */ - public function setSampled(?bool $sampled) - { - $this->sampled = $sampled; - - return $this; - } - - /** - * Gets a map of arbitrary data. - * - * @return array - */ - public function getData(): array - { - return $this->data; - } - - /** - * Sets a map of arbitrary data. This method will merge the given data with - * the existing one. - * - * @param array $data The data - * - * @return $this - */ - public function setData(array $data) - { - $this->data = array_merge($this->data, $data); - - return $this; - } - - /** - * Gets the data in a format suitable for storage in the "trace" context. - * - * @return array - * - * @psalm-return array{ - * data?: array, - * description?: string, - * op?: string, - * parent_span_id?: string, - * span_id: string, - * status?: string, - * tags?: array, - * trace_id: string - * } - */ - 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; - } - - 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; - } - - /** - * Sets the finish timestamp on the current span. - * - * @param float|null $endTimestamp Takes an endTimestamp if the end should not be the time when you call this function - * - * @return EventId|null Finish for a span always returns null - */ - public function finish(?float $endTimestamp = null): ?EventId - { - $this->endTimestamp = $endTimestamp ?? microtime(true); - - return null; - } - - /** - * Creates a new {@see Span} while setting the current ID as `parentSpanId`. - * Also the `sampled` decision will be inherited. - * - * @param SpanContext $context The context of the child span - */ - public function startChild(SpanContext $context): self - { - $context = clone $context; - $context->setSampled($this->sampled); - $context->setParentSpanId($this->spanId); - $context->setTraceId($this->traceId); - - $span = new self($context); - $span->transaction = $this->transaction; - $span->spanRecorder = $this->spanRecorder; - - if ($span->spanRecorder !== null) { - $span->spanRecorder->add($span); - } - - return $span; - } - - /** - * Gets the span recorder attached to this span. - * - * @internal - */ - public function getSpanRecorder(): ?SpanRecorder - { - return $this->spanRecorder; - } - - /** - * Detaches the span recorder from this instance. - * - * @return $this - */ - public function detachSpanRecorder() - { - $this->spanRecorder = null; - - return $this; - } - - /** - * @return array> - */ - public function getMetricsSummary(): array - { - return $this->metricsSummary; - } - - /** - * @param string|int|float $value - * @param string[] $tags - */ - public function setMetricsSummary( - string $type, - string $key, - $value, - MetricsUnit $unit, - array $tags - ): void { - $mri = sprintf('%s:%s@%s', $type, $key, (string) $unit); - $bucketKey = $mri . serialize($tags); - - if ( - isset($this->metricsSummary[$mri]) - && \array_key_exists($bucketKey, $this->metricsSummary[$mri]) - ) { - if ($type === SetType::TYPE) { - $value = 1.0; - } else { - $value = (float) $value; - } - - $summary = $this->metricsSummary[$mri][$bucketKey]; - $this->metricsSummary[$mri][$bucketKey] = [ - 'min' => min($summary['min'], $value), - 'max' => max($summary['max'], $value), - 'sum' => $summary['sum'] + $value, - 'count' => $summary['count'] + 1, - 'tags' => $tags, - ]; - } else { - if ($type === SetType::TYPE) { - $value = 0.0; - } else { - $value = (float) $value; - } - - $this->metricsSummary[$mri][$bucketKey] = [ - 'min' => $value, - 'max' => $value, - 'sum' => $value, - 'count' => 1, - 'tags' => $tags, - ]; - } - } - - /** - * Returns the transaction containing this span. - */ - public function getTransaction(): ?Transaction - { - return $this->transaction; - } - - /** - * Returns a string that can be used for the `sentry-trace` header & meta tag. - */ - public function toTraceparent(): string - { - $sampled = ''; - - if ($this->sampled !== null) { - $sampled = $this->sampled ? '-1' : '-0'; - } - - return sprintf('%s-%s%s', (string) $this->traceId, (string) $this->spanId, $sampled); - } - - /** - * Returns a string that can be used for the W3C `traceparent` header & meta tag. - */ - public function toW3CTraceparent(): string - { - $sampled = ''; - - if ($this->sampled !== null) { - $sampled = $this->sampled ? '01' : '00'; - } else { - // If no sampling decision was made, set the flag to 00 - $sampled = '00'; - } - - return sprintf('00-%s-%s-%s', (string) $this->traceId, (string) $this->spanId, $sampled); - } - - /** - * Returns a string that can be used for the `baggage` header & meta tag. - */ - public function toBaggage(): string - { - $transaction = $this->getTransaction(); - - if ($transaction !== null) { - return (string) $transaction->getDynamicSamplingContext(); - } - - return ''; - } -} diff --git a/src/Tracing/Spans/SegmentSpan.php b/src/Tracing/Spans/SegmentSpan.php deleted file mode 100644 index 4f85aa707..000000000 --- a/src/Tracing/Spans/SegmentSpan.php +++ /dev/null @@ -1,13 +0,0 @@ -[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; + private $hub; + public $traceId; + + public $segmentId; + + public $spanId; + + public $parentSpanId; + + public $isSegment; + public $startTimestamp; public $endTimestamp; @@ -25,25 +38,24 @@ class Span public $status; - public $data; + public $tags = []; - public $traceId; + public $data = []; - public $segmentId; - - public $spanId; + public $context = []; - public $isSegment; + public $metadata = []; + + public $metricsSummary = []; - private function __construct() + public function __construct() { $this->hub = SentrySdk::getCurrentHub(); $this->traceId = TraceId::generate(); - $this->segmentId = SegmentId::generate(); + // $this->segmentId = SegmentId::generate(); $this->spanId = SpanId::generate(); - - $this->isSegment = true; + $this->segmentId = $this->spanId; } public static function make(): self @@ -51,10 +63,32 @@ public static function make(): self return new self(); } + public static function makeFromTrace(string $sentryTrace, string $baggage): self + { + $span = new self(); + + self::parseTraceAndBaggage($span, $sentryTrace, $baggage); + + return $span; + } + public function start(): self { $this->startTimestamp = microtime(true); + $parentSpan = $this->hub->getSpan(); + if ($parentSpan !== null) { + $this->parentSpanId = $parentSpan->spanId; + $this->traceId = $parentSpan->traceId; + $this->segmentId = $parentSpan->segmentId; + $this->isSegment = false; + } else { + $this->isSegment = true; + } + + + $this->hub->setSpan($this); + return $this; } @@ -72,9 +106,48 @@ public function setDescription(string $description): self return $this; } + public function setTags(array $tags): self + { + $this->tags = array_merge($this->tags, $tags); + + return $this; + } + + public function setData(array $data) + { + $this->data = array_merge($this->data, $data); + + return $this; + } + + public function setContext(array $context) + { + $this->context = array_merge($this->context, $context); + + return $this; + } + + public function setStartTimestamp(?float $startTimestamp): self + { + $this->startTimestamp = $startTimestamp; + + return $this; + } + + public function setEndTimestamp(?float $endTimestamp): self + { + $this->endTimestamp = $endTimestamp; + + return $this; + } public function finish(): ?EventId { + if ($this->endTimestamp !== null) { + // The span was already finished once and we don't want to re-flush it + return null; + } + $this->endTimestamp = microtime(true); $this->exclusiveTime = $this->endTimestamp - $this->startTimestamp; @@ -85,4 +158,72 @@ public function finish(): ?EventId return $this->hub->captureEvent($event); } + + 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; + } + + // TBD 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; + } + + private static function parseTraceAndBaggage(Span $span, string $sentryTrace, string $baggage) + { + $hasSentryTrace = false; + + if (preg_match(self::SENTRY_TRACEPARENT_HEADER_REGEX, $sentryTrace, $matches)) { + if (!empty($matches['trace_id'])) { + $span->traceId = new TraceId($matches['trace_id']); + $hasSentryTrace = true; + } + + if (!empty($matches['span_id'])) { + $span->parentSpanId = new SpanId($matches['span_id']); + $hasSentryTrace = true; + } + } + + $samplingContext = DynamicSamplingContext::fromHeader($baggage); + + if ($hasSentryTrace && !$samplingContext->hasEntries()) { + // The request comes from an old SDK which does not support Dynamic Sampling. + // Propagate the Dynamic Sampling Context as is, but frozen, even without sentry-* entries. + $samplingContext->freeze(); + $span->metadata['dynamic_sampling_context'] = $samplingContext; + } + + if ($hasSentryTrace && $samplingContext->hasEntries()) { + // The baggage header contains Dynamic Sampling Context data from an upstream SDK. + // Propagate this Dynamic Sampling Context. + $span->metadata['dynamic_sampling_context'] = $samplingContext; + } + } } \ No newline at end of file From 71714a513e03f8a63c0fc67d527fabf0aef7a95d Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Mon, 29 Apr 2024 16:14:21 +0200 Subject: [PATCH 3/9] New item type --- src/EventType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventType.php b/src/EventType.php index a25d3f473..c3e7089b8 100644 --- a/src/EventType.php +++ b/src/EventType.php @@ -39,7 +39,7 @@ public static function transaction(): self public static function span(): self { - return self::getInstance('span'); + return self::getInstance('otel_span'); } public static function checkIn(): self From b4b35bf637d9bbde0eddd14b8c55340defc8ec01 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 2 May 2024 18:34:13 +0200 Subject: [PATCH 4/9] Make it somehow work --- src/Profiling/Profile.php | 44 +++--- src/Serializer/EnvelopItems/ProfileItem.php | 3 +- src/Serializer/EnvelopItems/SpanItem.php | 153 ++++++++++++-------- src/Tracing/Spans/Span.php | 126 ++++++---------- 4 files changed, 163 insertions(+), 163 deletions(-) diff --git a/src/Profiling/Profile.php b/src/Profiling/Profile.php index 06d895558..ad7a09e0a 100644 --- a/src/Profiling/Profile.php +++ b/src/Profiling/Profile.php @@ -82,6 +82,7 @@ final class Profile /** * @var string The version of the profile format + * @TODO(michi) VERSION::2 */ private const VERSION = '1'; @@ -249,6 +250,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), ]; } @@ -267,30 +269,30 @@ public function getFormattedData(Event $event): ?array } return [ - 'device' => [ - 'architecture' => $osContext->getMachineType(), - ], - 'event_id' => $this->eventId ? (string) $this->eventId : SentryUid::generate(), - 'os' => [ - 'name' => $osContext->getName(), - 'version' => $osContext->getVersion(), - 'build_number' => $osContext->getBuild() ?? '', - ], + // 'device' => [ + // 'architecture' => $osContext->getMachineType(), + // ], + // 'event_id' => $this->eventId ? (string) $this->eventId : SentryUid::generate(), + // 'os' => [ + // 'name' => $osContext->getName(), + // 'version' => $osContext->getVersion(), + // 'build_number' => $osContext->getBuild() ?? '', + // ], 'platform' => 'php', 'release' => $event->getRelease() ?? '', 'environment' => $event->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, - 'runtime' => [ - 'name' => $runtimeContext->getName(), - 'sapi' => $runtimeContext->getSAPI(), - 'version' => $runtimeContext->getVersion(), - ], - 'timestamp' => $startTime->format(\DATE_RFC3339_EXTENDED), - 'transaction' => [ - 'id' => (string) $event->getId(), - 'name' => $event->getTransaction(), - 'trace_id' => $event->getTraceId(), - 'active_thread_id' => self::THREAD_ID, - ], + // 'runtime' => [ + // 'name' => $runtimeContext->getName(), + // 'sapi' => $runtimeContext->getSAPI(), + // 'version' => $runtimeContext->getVersion(), + // ], + // 'timestamp' => $startTime->format(\DATE_RFC3339_EXTENDED), + // 'transaction' => [ + // 'id' => (string) $event->getId(), + // 'name' => $event->getTransaction(), + // 'trace_id' => $event->getTraceId(), + // 'active_thread_id' => self::THREAD_ID, + // ], 'version' => self::VERSION, 'profile' => [ 'frames' => $frames, diff --git a/src/Serializer/EnvelopItems/ProfileItem.php b/src/Serializer/EnvelopItems/ProfileItem.php index 90fdca9ec..3319a7ba1 100644 --- a/src/Serializer/EnvelopItems/ProfileItem.php +++ b/src/Serializer/EnvelopItems/ProfileItem.php @@ -16,7 +16,8 @@ class ProfileItem implements EnvelopeItemInterface public static function toEnvelopeItem(Event $event): string { $header = [ - 'type' => 'profile', + // @TODO(michi) profile_chunk + 'type' => 'profile_chunk', 'content_type' => 'application/json', ]; diff --git a/src/Serializer/EnvelopItems/SpanItem.php b/src/Serializer/EnvelopItems/SpanItem.php index 56eb0bd7e..00b24b2dc 100644 --- a/src/Serializer/EnvelopItems/SpanItem.php +++ b/src/Serializer/EnvelopItems/SpanItem.php @@ -32,79 +32,110 @@ public static function toEnvelopeItem(Event $event): string 'content_type' => 'application/json', ]; - $payload = [ - 'platform' => 'php', - 'sdk' => [ - 'name' => $event->getSdkIdentifier(), - 'version' => $event->getSdkVersion(), - ], - ]; - $span = $event->getSpan(); - $payload['start_timestamp'] = $span->startTimestamp; - $payload['timestamp'] = $span->endTimestamp; - $payload['exclusive_time'] = $span->exclusiveTime; - - $payload['trace_id'] = (string) $span->traceId; - $payload['segment_id'] = (string) $span->segmentId; - $payload['span_id'] = (string) $span->spanId; - - $payload['is_segment'] = $span->isSegment; - - if ($span->parentSpanId !== null) { - $payload['parent_span_id'] = (string) $span->parentSpanId; - } - - if ($span->description !== null) { - $payload['description'] = $span->description; - } - - if ($span->op !== null) { - $payload['op'] = $span->op; - } + $payload = [ + 'traceId' => (string) $span->traceId, + 'spanId' => (string) $span->spanId, + 'parentSpanId' => (string) $span->parentSpanId, + // @ToDo(michi) name is required + 'name' => $span->name ?? '', + 'startTimeUnixNano' => (int) floor($span->startTimeUnixNano * 1_000_000_000), + 'endTimeUnixNano' => (int) floor($span->endTimeUnixNano * 1_000_000_000), + // @ToDo(michi) tbd + 'kind' => 0, + ]; - if ($span->status !== null) { - $payload['status'] = $span->status; + foreach ($span->attributes as $attribute) { + $payload['attributes'][] = [ + 'key' => array_key_first($attribute), + 'value' => [ + 'stringValue' => $attribute[array_key_first($attribute)], + ], + ]; } - if (!empty($span->data)) { - $payload['data'] = $span->data; + if ($span->segmentSpan !== null) { + $payload['attributes'][] = [ + 'key' => 'sentry.segment.id', + 'value' => [ + 'stringValue' => (string) $span->segmentSpan->spanId, + ], + ]; + $payload['attributes'][] = [ + 'key' => 'sentry.segment.name', + 'value' => [ + 'stringValue' => $span->segmentSpan->name, + ], + ]; + // @TODO(michi) name is required + // $payload['attributes'][] = [ + // 'key' => 'sentry.segment.op', + // 'value' => [ + // 'stringValue' => $span->segmentSpan->spanId, + // ], + // ]; + } else { + $payload['attributes'][] = [ + 'key' => 'sentry.segment.id', + 'value' => [ + 'stringValue' => (string) $span->spanId, + ], + ]; + $payload['attributes'][] = [ + 'key' => 'sentry.segment.name', + 'value' => [ + 'stringValue' => $span->name, + ], + ]; } - // if (!empty($span->tags)) { - // $payload['tags'] = $span->tags; - // } - - // if (!empty($span->context)) { - // $payload['context'] = $span->context; - // } - - // if (!empty($span->metricsSummary)) { - // $payload['_metrics_summary'] = self::serializeMetricsSummary($span->metricsSummary); - // } - if ($event->getRelease() !== null) { - $payload['release'] = $event->getRelease(); + $payload['attributes'][] = [ + 'key' => 'sentry.release', + 'value' => [ + 'stringValue' => $event->getRelease(), + ], + ]; } - if ($event->getEnvironment() !== null) { - $payload['environment'] = $event->getEnvironment(); + $payload['attributes'][] = [ + 'key' => 'sentry.environment', + 'value' => [ + 'stringValue' => $event->getEnvironment(), + ], + ]; } - // In general, mainly use data as it makes the SDK simpler - // See https://github.com/getsentry/rfcs/blob/main/text/0116-sentry-semantic-conventions.md - - // TBD: description -> name (OTel does the same) ✅ - // TBD: status -> use only three, HTTP status as context ✅ - // TBD: transaction -> confusing -> data.sentry.segment.name (data.sentry.segment_name) ✅ - // TBD: trace-origin - // TBD: profiling -> data.sentry.profiler_id - // TBD: tags?? -> this could become sentry.tags... field at one point - - // TBD: exclusive_time can't be calculated for single spans - // see https://sentry.my.sentry.io/organizations/sentry/issues/797887/events/?query=sdk%3A%22sentry.javascript.browser%2F7.109.0%22 - // we will remove the requirement for exclusive_time on Relay + $payload['attributes'][] = [ + 'key' => 'sentry.platform', + 'value' => [ + 'stringValue' => 'php', + ], + ]; + $payload['attributes'][] = [ + 'key' => 'sentry.sdk.name', + 'value' => [ + 'stringValue' => $event->getSdkIdentifier(), + ], + ]; + $payload['attributes'][] = [ + 'key' => 'sentry.sdk.version', + 'value' => [ + 'stringValue' => $event->getSdkVersion(), + ], + ]; + // @TODO(michi): add exclusive_time + // $payload['attributes'][] = [ + // 'key' => 'sentry.exclusive_time_nano', + // 'value' => [ + // 'integerValue' => $event->getEnvironment(), + // ], + // ]; + + // @TODO(michi): trace-origin + // @TODO(michi): add sentry.profiler.id attribute + // @TODO(michi): tags return sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } diff --git a/src/Tracing/Spans/Span.php b/src/Tracing/Spans/Span.php index 5abc2bbd6..3d3d14d62 100644 --- a/src/Tracing/Spans/Span.php +++ b/src/Tracing/Spans/Span.php @@ -14,48 +14,44 @@ class Span { private const SENTRY_TRACEPARENT_HEADER_REGEX = '/^[ \\t]*(?[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; - private $hub; + public $name; - public $traceId; + public $context = []; - public $segmentId; + public $traceId; public $spanId; public $parentSpanId; - public $isSegment; - - public $startTimestamp; + public $kind; - public $endTimestamp; + public $startTimeUnixNano; - public $exclusiveTime; + public $endTimeUnixNano; - public $op; + public $attributes = []; - public $description; + public $events = []; public $status; - public $tags = []; + public $links = []; - public $data = []; + // Sentry specifics - public $context = []; + private $hub; - public $metadata = []; + public $segmentSpan; - public $metricsSummary = []; + public $metadata = []; public function __construct() { $this->hub = SentrySdk::getCurrentHub(); $this->traceId = TraceId::generate(); - // $this->segmentId = SegmentId::generate(); $this->spanId = SpanId::generate(); - $this->segmentId = $this->spanId; } public static function make(): self @@ -74,16 +70,14 @@ public static function makeFromTrace(string $sentryTrace, string $baggage): self public function start(): self { - $this->startTimestamp = microtime(true); + $this->startTimeUnixNano = microtime(true); $parentSpan = $this->hub->getSpan(); if ($parentSpan !== null) { $this->parentSpanId = $parentSpan->spanId; $this->traceId = $parentSpan->traceId; - $this->segmentId = $parentSpan->segmentId; - $this->isSegment = false; - } else { - $this->isSegment = true; + + $this->segmentSpan = $parentSpan->segmentSpan ?? $parentSpan; } @@ -92,65 +86,37 @@ public function start(): self return $this; } - public function setOp(string $op): self - { - $this->op = $op; - - return $this; - } - - public function setDescription(string $description): self - { - $this->description = $description; - - return $this; - } - - public function setTags(array $tags): self - { - $this->tags = array_merge($this->tags, $tags); - - return $this; - } - - public function setData(array $data) + public function setName(string $name): self { - $this->data = array_merge($this->data, $data); + $this->name = $name; return $this; } - public function setContext(array $context) + public function setStartTimeUnixNanosetStartTime(float $startTime): self { - $this->context = array_merge($this->context, $context); + $this->startTimeUnixNano = $startTime; return $this; } - public function setStartTimestamp(?float $startTimestamp): self + public function setAttribiute(string $key, $value): self { - $this->startTimestamp = $startTimestamp; - - return $this; - } - - public function setEndTimestamp(?float $endTimestamp): self - { - $this->endTimestamp = $endTimestamp; + $this->attributes[] = [ + $key => $value, + ]; return $this; } public function finish(): ?EventId { - if ($this->endTimestamp !== null) { + if ($this->endTimeUnixNano !== null) { // The span was already finished once and we don't want to re-flush it return null; } - $this->endTimestamp = microtime(true); - $this->exclusiveTime = $this->endTimestamp - $this->startTimestamp; - + $this->endTimeUnixNano = microtime(true); $this->status = (string) SpanStatus::ok(); $event = Event::createSpan(); @@ -171,26 +137,26 @@ public function getTraceContext(): array } // TBD 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; - } + // + // 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; } From 08a2aa22b34950921fd79eb54034056b022ac384 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 2 May 2024 18:55:36 +0200 Subject: [PATCH 5/9] Add Span::setEndTimeUnixNanosetStartTime --- src/Tracing/Spans/Span.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Tracing/Spans/Span.php b/src/Tracing/Spans/Span.php index 3d3d14d62..b6f0f0092 100644 --- a/src/Tracing/Spans/Span.php +++ b/src/Tracing/Spans/Span.php @@ -100,6 +100,13 @@ public function setStartTimeUnixNanosetStartTime(float $startTime): self return $this; } + public function setEndTimeUnixNanosetStartTime(float $endTime): self + { + $this->endTimeUnixNano = $endTime; + + return $this; + } + public function setAttribiute(string $key, $value): self { $this->attributes[] = [ From 8b6a8abdf7f2430ba3869c527056343c8e9064b1 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 2 May 2024 18:59:10 +0200 Subject: [PATCH 6/9] Add Span::toTraceparent --- src/Tracing/Spans/Span.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tracing/Spans/Span.php b/src/Tracing/Spans/Span.php index b6f0f0092..688dc752b 100644 --- a/src/Tracing/Spans/Span.php +++ b/src/Tracing/Spans/Span.php @@ -168,6 +168,11 @@ public function getTraceContext(): array return $result; } + public function toTraceparent(): string + { + return sprintf('%s-%s%s', (string) $this->traceId, (string) $this->spanId, '-1'); + } + private static function parseTraceAndBaggage(Span $span, string $sentryTrace, string $baggage) { $hasSentryTrace = false; From 979ac4ac8d6d01a225d7502963f4b08467b57647 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 2 May 2024 19:06:44 +0200 Subject: [PATCH 7/9] Remove metrics transaction tag --- src/Metrics/MetricsAggregator.php | 21 +++++++++++---------- src/Serializer/EnvelopItems/SpanItem.php | 4 ++-- src/Tracing/Spans/Span.php | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index 5014c97fe..b2eaa9ca9 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -135,16 +135,17 @@ private function serializeTags(array $tags): array $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(); - } - }); + // @TODO(michi) fix this + // $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); } diff --git a/src/Serializer/EnvelopItems/SpanItem.php b/src/Serializer/EnvelopItems/SpanItem.php index 00b24b2dc..9eaaba4ad 100644 --- a/src/Serializer/EnvelopItems/SpanItem.php +++ b/src/Serializer/EnvelopItems/SpanItem.php @@ -38,11 +38,11 @@ public static function toEnvelopeItem(Event $event): string 'traceId' => (string) $span->traceId, 'spanId' => (string) $span->spanId, 'parentSpanId' => (string) $span->parentSpanId, - // @ToDo(michi) name is required + // @TODO(michi) name is required 'name' => $span->name ?? '', 'startTimeUnixNano' => (int) floor($span->startTimeUnixNano * 1_000_000_000), 'endTimeUnixNano' => (int) floor($span->endTimeUnixNano * 1_000_000_000), - // @ToDo(michi) tbd + // @TODO(michi)) tbd 'kind' => 0, ]; diff --git a/src/Tracing/Spans/Span.php b/src/Tracing/Spans/Span.php index 688dc752b..a3b8a3408 100644 --- a/src/Tracing/Spans/Span.php +++ b/src/Tracing/Spans/Span.php @@ -143,7 +143,7 @@ public function getTraceContext(): array $result['parent_span_id'] = (string) $this->parentSpanId; } - // TBD do we need all this data on the trace context? + // @TODO(michi) do we need all this data on the trace context? // // if ($this->description !== null) { // $result['description'] = $this->description; From acbb89f1ef536eff2b5de23ae4f65abb0eb57bd3 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 2 May 2024 19:08:08 +0200 Subject: [PATCH 8/9] Remove span metrics aggregation --- src/Metrics/MetricsAggregator.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index b2eaa9ca9..d99eaea59 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -94,9 +94,10 @@ public function add( } $span = $hub->getSpan(); - if ($span !== null) { - $span->setMetricsSummary($type, $key, $value, $unit, $tags); - } + // @TODO(michi) fix this + // if ($span !== null) { + // $span->setMetricsSummary($type, $key, $value, $unit, $tags); + // } } public function flush(): ?EventId From d422a1b89f4bab503b662fe9e83a8cc1bcd1ae02 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Fri, 3 May 2024 17:13:52 +0200 Subject: [PATCH 9/9] WIP --- src/Serializer/EnvelopItems/SpanItem.php | 33 +++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Serializer/EnvelopItems/SpanItem.php b/src/Serializer/EnvelopItems/SpanItem.php index 9eaaba4ad..0e9ca8f95 100644 --- a/src/Serializer/EnvelopItems/SpanItem.php +++ b/src/Serializer/EnvelopItems/SpanItem.php @@ -4,6 +4,7 @@ namespace Sentry\Serializer\EnvelopItems; +use Exception; use Sentry\Event; use Sentry\Serializer\Traits\BreadcrumbSeralizerTrait; use Sentry\Tracing\Span; @@ -47,10 +48,38 @@ public static function toEnvelopeItem(Event $event): string ]; foreach ($span->attributes as $attribute) { + $key = array_key_first($attribute); + $value = $attribute[$key]; + $type = gettype($value); + + switch ($type) { + case 'boolean': + $valueType = 'booleanValue'; + break; + case 'integer': + $valueType = 'integerValue'; + break; + case 'double': + $valueType = 'floatValue'; + break; + case 'string': + $valueType = 'stringValue'; + break; + default: + // @TODO(michi) this is temporary + throw new Exception( + sprintf( + 'Unknown attribute value type passed "%" for key "%"', + $type, + $key + ) + ); + } + $payload['attributes'][] = [ 'key' => array_key_first($attribute), 'value' => [ - 'stringValue' => $attribute[array_key_first($attribute)], + $valueType => $attribute[array_key_first($attribute)], ], ]; } @@ -137,6 +166,8 @@ public static function toEnvelopeItem(Event $event): string // @TODO(michi): add sentry.profiler.id attribute // @TODO(michi): tags + debug($payload); + return sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); }