Skip to content

Commit d6f5e3e

Browse files
fix bug where span is not ended/scope is not detached, when client operation promise rejects (open-telemetry#357)
1 parent 4a740a5 commit d6f5e3e

File tree

1 file changed

+81
-25
lines changed

1 file changed

+81
-25
lines changed

src/Aws/src/AwsSdkInstrumentation.php

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44

55
namespace OpenTelemetry\Aws;
66

7+
use Aws\CommandInterface;
78
use Aws\Middleware;
89
use Aws\ResultInterface;
10+
use Closure;
11+
use Error;
12+
use GuzzleHttp\Promise;
913
use OpenTelemetry\API\Instrumentation\InstrumentationInterface;
1014
use OpenTelemetry\API\Instrumentation\InstrumentationTrait;
1115
use OpenTelemetry\API\Trace\SpanKind;
16+
use OpenTelemetry\API\Trace\StatusCode;
1217
use OpenTelemetry\API\Trace\TracerInterface;
1318
use OpenTelemetry\API\Trace\TracerProviderInterface;
1419
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
20+
use Psr\Http\Message\RequestInterface;
21+
use Throwable;
1522

1623
/**
1724
* @experimental
@@ -114,37 +121,86 @@ public function activate(): bool
114121
]);
115122
}), 'instrumentation');
116123

117-
$client->getHandlerList()->appendSign(Middleware::mapResult(function (ResultInterface $result) use ($hash) {
118-
if (empty($this->spanStorage[$hash])) {
119-
return $result;
120-
}
121-
[$span, $scope] = $this->spanStorage[$hash];
122-
unset($this->spanStorage[$hash]);
123-
124-
/*
125-
* Some AWS SDK Functions, such as S3Client->getObjectUrl() do not actually perform on the wire comms
126-
* with AWS Servers, and therefore do not return with a populated AWS\Result object with valid @metadata
127-
* Check for the presence of @metadata before extracting status code as these calls are still
128-
* instrumented.
129-
*/
130-
if (isset($result['@metadata'])) {
131-
$span->setAttributes([
132-
'http.status_code' => $result['@metadata']['statusCode'], // @phan-suppress-current-line PhanTypeMismatchDimFetch
133-
]);
134-
}
135-
136-
$span->end();
137-
$scope->detach();
138-
139-
return $result;
140-
}), 'end_instrumentation');
124+
$client->getHandlerList()->appendSign(function (callable $handler) use ($hash) {
125+
return $this->endSpanMiddleware($handler, $hash);
126+
}, 'end_instrumentation');
141127

142128
$this->instrumentedClients[$hash] = 1;
143129
}
144-
} catch (\Throwable $e) {
130+
} catch (Throwable $e) {
145131
return false;
146132
}
147133

148134
return true;
149135
}
136+
137+
private function endSpanMiddleware(callable $handler, string $hash): Closure
138+
{
139+
$onFulfilled = function (ResultInterface $result) use ($hash) {
140+
if (empty($this->spanStorage[$hash])) {
141+
return $result;
142+
}
143+
[$span, $scope] = $this->spanStorage[$hash];
144+
unset($this->spanStorage[$hash]);
145+
146+
/*
147+
* Some AWS SDK Functions, such as S3Client->getObjectUrl() do not actually perform on the wire comms
148+
* with AWS Servers, and therefore do not return with a populated AWS\Result object with valid @metadata
149+
* Check for the presence of @metadata before extracting status code as these calls are still
150+
* instrumented.
151+
*/
152+
if (isset($result['@metadata'])) {
153+
$span->setAttributes([
154+
'http.status_code' => $result['@metadata']['statusCode'], // @phan-suppress-current-line PhanTypeMismatchDimFetch
155+
]);
156+
}
157+
158+
$span->end();
159+
$scope->detach();
160+
161+
return $result;
162+
};
163+
164+
$onRejected = function ($reason) use ($hash) {
165+
if (empty($this->spanStorage[$hash])) {
166+
return Promise\Create::rejectionFor($reason);
167+
}
168+
[$span, $scope] = $this->spanStorage[$hash];
169+
unset($this->spanStorage[$hash]);
170+
171+
$span->setStatus(StatusCode::STATUS_ERROR, $this->normalizeReason($reason));
172+
173+
if ($reason instanceof Throwable) {
174+
$span->recordException($reason);
175+
}
176+
177+
$span->end();
178+
$scope->detach();
179+
180+
return Promise\Create::rejectionFor($reason);
181+
};
182+
183+
return function (
184+
CommandInterface $command,
185+
?RequestInterface $request = null,
186+
) use ($handler, $onFulfilled, $onRejected) {
187+
return $handler($command, $request)->then(
188+
$onFulfilled,
189+
$onRejected,
190+
);
191+
};
192+
}
193+
194+
private function normalizeReason(mixed $reason): ?string
195+
{
196+
if ($reason instanceof Throwable) {
197+
return $reason->getMessage();
198+
}
199+
200+
try {
201+
return strval($reason);
202+
} catch (Error) {
203+
return null;
204+
}
205+
}
150206
}

0 commit comments

Comments
 (0)