88use Aws \ResultInterface ;
99use OpenTelemetry \API \Instrumentation \InstrumentationInterface ;
1010use OpenTelemetry \API \Instrumentation \InstrumentationTrait ;
11- use OpenTelemetry \API \Trace \SpanInterface ;
1211use OpenTelemetry \API \Trace \SpanKind ;
1312use OpenTelemetry \API \Trace \TracerInterface ;
1413use OpenTelemetry \API \Trace \TracerProviderInterface ;
1514use OpenTelemetry \Context \Propagation \TextMapPropagatorInterface ;
16- use OpenTelemetry \Context \ScopeInterface ;
1715
1816/**
1917 * @experimental
@@ -25,13 +23,12 @@ class AwsSdkInstrumentation implements InstrumentationInterface
2523 public const NAME = 'AWS SDK Instrumentation ' ;
2624 public const VERSION = '0.0.1 ' ;
2725 public const SPAN_KIND = SpanKind::KIND_CLIENT ;
28- private TextMapPropagatorInterface $ propagator ;
29- private TracerProviderInterface $ tracerProvider ;
30- private $ clients = [] ;
31- private string $ clientName ;
32- private string $ region ;
33- private SpanInterface $ span ;
34- private ScopeInterface $ scope ;
26+
27+ private array $ clients = [];
28+
29+ private array $ instrumentedClients = [];
30+
31+ private array $ spanStorage = [];
3532
3633 public function getName (): string
3734 {
@@ -79,61 +76,70 @@ public function getTracer(): TracerInterface
7976 }
8077
8178 /** @psalm-api */
82- public function instrumentClients ($ clientsArray ) : void
79+ public function instrumentClients ($ clientsArray ): void
8380 {
8481 $ this ->clients = $ clientsArray ;
8582 }
8683
87- /** @psalm-suppress ArgumentTypeCoercion */
8884 public function activate (): bool
8985 {
9086 try {
91- $ middleware = Middleware::tap (function ($ cmd , $ _req ) {
92- $ tracer = $ this ->getTracer ();
93- $ propagator = $ this ->getPropagator ();
94-
95- $ carrier = [];
96- /** @phan-suppress-next-line PhanTypeMismatchArgument */
97- $ this ->span = $ tracer ->spanBuilder ($ this ->clientName )->setSpanKind (AwsSdkInstrumentation::SPAN_KIND )->startSpan ();
98- $ this ->scope = $ this ->span ->activate ();
99-
100- $ propagator ->inject ($ carrier );
101-
102- /** @psalm-suppress PossiblyInvalidArgument */
103- $ this ->span ->setAttributes ([
104- 'rpc.method ' => $ cmd ->getName (),
105- 'rpc.service ' => $ this ->clientName ,
106- 'rpc.system ' => 'aws-api ' ,
107- 'aws.region ' => $ this ->region ,
108- ]);
109- });
110-
111- /** @psalm-suppress PossiblyInvalidArgument */
112- $ end_middleware = Middleware::mapResult (function (ResultInterface $ result ) {
113- /**
114- * Some AWS SDK Funtions, such as S3Client->getObjectUrl() do not actually perform on the wire comms
115- * with AWS Servers, and therefore do not return with a populated AWS\Result object with valid @metadata
116- * Check for the presence of @metadata before extracting status code as these calls are still
117- * instrumented.
118- */
119- if (isset ($ result ['@metadata ' ])) {
120- $ this ->span ->setAttributes ([
121- 'http.status_code ' => $ result ['@metadata ' ]['statusCode ' ], //@phan-suppress-current-line PhanTypeMismatchDimFetch
122- ]);
87+ foreach ($ this ->clients as $ client ) {
88+ $ hash = spl_object_hash ($ client );
89+ if (isset ($ this ->instrumentedClients [$ hash ])) {
90+ continue ;
12391 }
12492
125- $ this -> span -> end ();
126- $ this -> scope -> detach ();
93+ $ clientName = $ client -> getApi ()-> getServiceName ();
94+ $ region = $ client -> getRegion ();
12795
128- return $ result ;
129- });
96+ $ client ->getHandlerList ()->prependInit (Middleware::tap (function ($ cmd , $ _req ) use ($ clientName , $ region , $ hash ) {
97+ $ tracer = $ this ->getTracer ();
98+ $ propagator = $ this ->getPropagator ();
13099
131- foreach ($ this ->clients as $ client ) {
132- $ this ->clientName = $ client ->getApi ()->getServiceName ();
133- $ this ->region = $ client ->getRegion ();
100+ $ carrier = [];
101+ /** @phan-suppress-next-line PhanTypeMismatchArgument */
102+ $ span = $ tracer ->spanBuilder ($ clientName )->setSpanKind (AwsSdkInstrumentation::SPAN_KIND )->startSpan ();
103+ $ scope = $ span ->activate ();
104+ $ this ->spanStorage [$ hash ] = [$ span , $ scope ];
134105
135- $ client ->getHandlerList ()->prependInit ($ middleware , 'instrumentation ' );
136- $ client ->getHandlerList ()->appendSign ($ end_middleware , 'end_instrumentation ' );
106+ $ propagator ->inject ($ carrier );
107+
108+ /** @psalm-suppress PossiblyInvalidArgument */
109+ $ span ->setAttributes ([
110+ 'rpc.method ' => $ cmd ->getName (),
111+ 'rpc.service ' => $ clientName ,
112+ 'rpc.system ' => 'aws-api ' ,
113+ 'aws.region ' => $ region ,
114+ ]);
115+ }), 'instrumentation ' );
116+
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 ' );
141+
142+ $ this ->instrumentedClients [$ hash ] = 1 ;
137143 }
138144 } catch (\Throwable $ e ) {
139145 return false ;
0 commit comments