From 6f20b787a296a468f0a4861c7e545bed342b2994 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:53:45 +0000 Subject: [PATCH] feat: auto-set level attribute on spans Set level=error when setError() is called, level=info otherwise. Explicit level via set('level', ...) is never overridden. Co-Authored-By: Claude Opus 4.6 --- README.md | 8 ++++++++ src/Span/Exporter/Sentry.php | 9 +++++---- src/Span/Span.php | 4 ++++ tests/SpanTest.php | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48e7052..ec47257 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Spans automatically include these attributes: | `span.started_at` | Start timestamp in seconds (float) | | `span.finished_at` | End timestamp in seconds (float) | | `span.duration` | Duration in seconds (float) | +| `level` | `error` if error set, `info` otherwise | ### Static Helpers @@ -78,6 +79,13 @@ try { Exporters access the exception via `$span->getError()` and extract what they need (message, trace, etc.). +The `level` attribute is automatically set to `error` when an error is captured. You can override it: + +```php +$span->setError($e); +$span->set('level', 'warning'); // override auto-detected level +``` + ### Distributed Tracing Propagate trace context across services using W3C Trace Context headers: diff --git a/src/Span/Exporter/Sentry.php b/src/Span/Exporter/Sentry.php index 59ee00c..fd79f5c 100644 --- a/src/Span/Exporter/Sentry.php +++ b/src/Span/Exporter/Sentry.php @@ -120,7 +120,6 @@ public function export(Span $span): void } } - curl_close($ch); } private function buildEnvelope(Span $span): ?string @@ -205,7 +204,7 @@ private function buildEnvelope(Span $span): ?string } $payloadData = [ - 'level' => 'error', + 'level' => (string) $attributes['level'], 'platform' => 'php', 'sdk' => [ 'name' => 'utopia-php/span', @@ -278,11 +277,13 @@ private function classifyAttributes(array $attributes): array $extra = []; foreach ($attributes as $key => $value) { - // Skip internal span attributes + // Skip internal span attributes and level (handled in payload) if (str_starts_with((string) $key, 'span.')) { continue; } - + if ($key === 'level') { + continue; + } // Skip only the HTTP attributes we handle in buildRequest/buildResponse if (\in_array($key, self::HANDLED_HTTP_KEYS, true)) { continue; diff --git a/src/Span/Span.php b/src/Span/Span.php index d932262..b519b64 100644 --- a/src/Span/Span.php +++ b/src/Span/Span.php @@ -266,6 +266,10 @@ public function finish(): void $this->attributes['span.finished_at'] = $finishedAt; $this->attributes['span.duration'] = $finishedAt - $startedAt; + if (!isset($this->attributes['level'])) { + $this->attributes['level'] = $this->error instanceof \Throwable ? 'error' : 'info'; + } + foreach (self::$exporters as $config) { try { $exporter = $config['exporter']; diff --git a/tests/SpanTest.php b/tests/SpanTest.php index 62c3cc8..bdee001 100644 --- a/tests/SpanTest.php +++ b/tests/SpanTest.php @@ -558,6 +558,40 @@ public function testInitWithInvalidTraceparentCreatesNewTrace(): void $this->assertNull($span->get('span.parent_id')); } + public function testFinishSetsLevelInfoByDefault(): void + { + $span = new Span(); + $span->finish(); + + $this->assertSame('info', $span->get('level')); + } + + public function testFinishSetsLevelErrorWhenErrorSet(): void + { + $span = new Span(); + $span->setError(new RuntimeException('Test')); + $span->finish(); + + $this->assertSame('error', $span->get('level')); + } + + public function testFinishDoesNotOverrideExplicitLevel(): void + { + $span = new Span(); + $span->set('level', 'warning'); + $span->setError(new RuntimeException('Test')); + $span->finish(); + + $this->assertSame('warning', $span->get('level')); + } + + public function testLevelNotSetBeforeFinish(): void + { + $span = new Span(); + + $this->assertNull($span->get('level')); + } + /** * @param array $exported */