@@ -45,8 +45,10 @@ using ::google::cloud::testing_util::EventNamed;
4545using ::google::cloud::testing_util::InstallSpanCatcher;
4646using ::google::cloud::testing_util::IsOk;
4747using ::google::cloud::testing_util::IsOkAndHolds;
48+ using ::google::cloud::testing_util::OTelAttribute;
4849using ::google::cloud::testing_util::OTelContextCaptured;
4950using ::google::cloud::testing_util::PromiseWithOTelContext;
51+ using ::google::cloud::testing_util::SpanHasAttributes;
5052using ::google::cloud::testing_util::SpanHasEvents;
5153using ::google::cloud::testing_util::SpanHasInstrumentationScope;
5254using ::google::cloud::testing_util::SpanKindIsClient;
@@ -763,6 +765,209 @@ TEST(ConnectionTracing, GetBucketError) {
763765 SpanHasInstrumentationScope (), SpanKindIsClient ())));
764766}
765767
768+ TEST (ConnectionTracing, GetBucketSpanEnrichment) {
769+ auto span_catcher = InstallSpanCatcher ();
770+ PromiseWithOTelContext<StatusOr<google::storage::v2::Bucket>> p;
771+
772+ auto options =
773+ TracingEnabled ()
774+ .set <google::cloud::storage_experimental::OTelSpanEnrichmentOption>(
775+ true );
776+ auto mock = std::make_unique<MockAsyncConnection>();
777+ EXPECT_CALL (*mock, options).WillRepeatedly (Return (options));
778+ EXPECT_CALL (*mock, GetBucket).WillOnce (expect_context (p));
779+ auto actual = MakeTracingAsyncConnection (std::move (mock));
780+
781+ google::storage::v2::GetBucketRequest req;
782+ req.set_name (" projects/_/buckets/test-bucket" );
783+ auto result =
784+ actual->GetBucket ({std::move (req), options}).then (expect_no_context);
785+
786+ google::storage::v2::Bucket bucket_meta;
787+ bucket_meta.set_project (" projects/123456" );
788+ bucket_meta.set_location (" us-east1" );
789+ bucket_meta.set_location_type (" regional" );
790+ p.set_value (make_status_or (std::move (bucket_meta)));
791+ ASSERT_STATUS_OK (result.get ());
792+
793+ auto spans = span_catcher->GetSpans ();
794+ EXPECT_THAT (
795+ spans,
796+ ElementsAre (AllOf (
797+ SpanNamed (" storage::AsyncConnection::GetBucket" ),
798+ SpanWithStatus (opentelemetry::trace::StatusCode::kOk ),
799+ SpanHasInstrumentationScope (), SpanKindIsClient (),
800+ SpanHasAttributes (
801+ OTelAttribute<std::string>(" gcp.resource.destination.id" ,
802+ " projects/123456/buckets/test-bucket" ),
803+ OTelAttribute<std::string>(" gcp.resource.destination.location" ,
804+ " us-east1" )))));
805+ }
806+
807+ TEST (ConnectionTracing, BucketMetadataCacheSuccess) {
808+ auto span_catcher = InstallSpanCatcher ();
809+ PromiseWithOTelContext<Status> delete_promise_1;
810+ PromiseWithOTelContext<StatusOr<google::storage::v2::Bucket>> bucket_promise;
811+ PromiseWithOTelContext<Status> delete_promise_2;
812+
813+ auto options =
814+ TracingEnabled ()
815+ .set <google::cloud::storage_experimental::OTelSpanEnrichmentOption>(
816+ true );
817+
818+ auto mock = std::make_unique<MockAsyncConnection>();
819+ EXPECT_CALL (*mock, options).WillRepeatedly (Return (options));
820+
821+ // 1st call: DeleteObject on a cache miss triggers background GetBucket
822+ EXPECT_CALL (*mock, DeleteObject)
823+ .WillOnce (expect_context (delete_promise_1))
824+ .WillOnce (expect_context (delete_promise_2));
825+
826+ EXPECT_CALL (*mock, GetBucket).WillOnce ([&](auto const &) {
827+ return bucket_promise.get_future ();
828+ });
829+
830+ auto actual = MakeTracingAsyncConnection (std::move (mock));
831+
832+ google::storage::v2::DeleteObjectRequest req;
833+ req.set_bucket (" projects/_/buckets/test-bucket" );
834+ req.set_object (" test-object-1" );
835+
836+ auto res1 = actual->DeleteObject ({req, options}).then (expect_no_context);
837+ delete_promise_1.set_value (Status{});
838+ ASSERT_STATUS_OK (res1.get ());
839+
840+ // Complete the background GetBucket fetch to populate the cache
841+ google::storage::v2::Bucket bucket_meta;
842+ bucket_meta.set_project (" projects/123456" );
843+ bucket_meta.set_location (" us-east1" );
844+ bucket_meta.set_location_type (" regional" );
845+ bucket_promise.set_value (make_status_or (std::move (bucket_meta)));
846+
847+ // Clear spans from the first operation and background GetBucket
848+ (void )span_catcher->GetSpans ();
849+
850+ // 2nd call: DeleteObject hits the cache and span is enriched
851+ req.set_object (" test-object-2" );
852+ auto res2 = actual->DeleteObject ({req, options}).then (expect_no_context);
853+ delete_promise_2.set_value (Status{});
854+ ASSERT_STATUS_OK (res2.get ());
855+
856+ auto spans = span_catcher->GetSpans ();
857+ EXPECT_THAT (
858+ spans,
859+ ElementsAre (AllOf (
860+ SpanNamed (" storage::AsyncConnection::DeleteObject" ),
861+ SpanWithStatus (opentelemetry::trace::StatusCode::kOk ),
862+ SpanHasInstrumentationScope (), SpanKindIsClient (),
863+ SpanHasAttributes (
864+ OTelAttribute<std::string>(" gcp.resource.destination.id" ,
865+ " projects/123456/buckets/test-bucket" ),
866+ OTelAttribute<std::string>(" gcp.resource.destination.location" ,
867+ " us-east1" )))));
868+ }
869+
870+ TEST (ConnectionTracing, GetBucketMaybeInvalidateEvicts) {
871+ auto span_catcher = InstallSpanCatcher ();
872+ PromiseWithOTelContext<StatusOr<google::storage::v2::Bucket>> p1;
873+ PromiseWithOTelContext<StatusOr<google::storage::v2::Bucket>> p2;
874+
875+ auto options =
876+ TracingEnabled ()
877+ .set <google::cloud::storage_experimental::OTelSpanEnrichmentOption>(
878+ true );
879+ auto mock = std::make_unique<MockAsyncConnection>();
880+ EXPECT_CALL (*mock, options).WillRepeatedly (Return (options));
881+
882+ EXPECT_CALL (*mock, GetBucket)
883+ .WillOnce (expect_context (p1))
884+ .WillOnce (expect_context (p2));
885+
886+ auto actual = MakeTracingAsyncConnection (std::move (mock));
887+
888+ // 1st call populates the cache
889+ google::storage::v2::GetBucketRequest req;
890+ req.set_name (" projects/_/buckets/test-bucket" );
891+ auto res1 = actual->GetBucket ({req, options}).then (expect_no_context);
892+ google::storage::v2::Bucket bucket_meta;
893+ bucket_meta.set_project (" projects/123456" );
894+ bucket_meta.set_location (" us-east1" );
895+ bucket_meta.set_location_type (" regional" );
896+ p1.set_value (make_status_or (std::move (bucket_meta)));
897+ ASSERT_STATUS_OK (res1.get ());
898+
899+ // 2nd call returns kNotFound and evicts the cached metadata
900+ auto res2 = actual->GetBucket ({req, options}).then (expect_no_context);
901+ p2.set_value (StatusOr<google::storage::v2::Bucket>(
902+ Status (StatusCode::kNotFound , " not found" )));
903+ EXPECT_THAT (res2.get (), StatusIs (StatusCode::kNotFound ));
904+ }
905+
906+ TEST (ConnectionTracing, DeleteObjectNoEvictOnError) {
907+ auto span_catcher = InstallSpanCatcher ();
908+ PromiseWithOTelContext<StatusOr<google::storage::v2::Bucket>> bucket_p;
909+ PromiseWithOTelContext<Status> delete_p1;
910+ PromiseWithOTelContext<Status> delete_p2;
911+
912+ auto options =
913+ TracingEnabled ()
914+ .set <google::cloud::storage_experimental::OTelSpanEnrichmentOption>(
915+ true );
916+ auto mock = std::make_unique<MockAsyncConnection>();
917+ EXPECT_CALL (*mock, options).WillRepeatedly (Return (options));
918+
919+ EXPECT_CALL (*mock, GetBucket).WillOnce (expect_context (bucket_p));
920+ EXPECT_CALL (*mock, DeleteObject)
921+ .WillOnce (expect_context (delete_p1))
922+ .WillOnce (expect_context (delete_p2));
923+
924+ auto actual = MakeTracingAsyncConnection (std::move (mock));
925+
926+ // 1st call: GetBucket populates the cache
927+ google::storage::v2::GetBucketRequest bucket_req;
928+ bucket_req.set_name (" projects/_/buckets/test-bucket" );
929+ auto res_bucket =
930+ actual->GetBucket ({bucket_req, options}).then (expect_no_context);
931+ google::storage::v2::Bucket bucket_meta;
932+ bucket_meta.set_project (" projects/123456" );
933+ bucket_meta.set_location (" us-east1" );
934+ bucket_meta.set_location_type (" regional" );
935+ bucket_p.set_value (make_status_or (std::move (bucket_meta)));
936+ ASSERT_STATUS_OK (res_bucket.get ());
937+
938+ // 2nd call: DeleteObject fails with kNotFound (object missing)
939+ google::storage::v2::DeleteObjectRequest del_req;
940+ del_req.set_bucket (" projects/_/buckets/test-bucket" );
941+ del_req.set_object (" missing-object" );
942+ auto res_del1 =
943+ actual->DeleteObject ({del_req, options}).then (expect_no_context);
944+ delete_p1.set_value (Status (StatusCode::kNotFound , " not found" ));
945+ EXPECT_THAT (res_del1.get (), StatusIs (StatusCode::kNotFound ));
946+
947+ // Clear spans to verify next span enrichment
948+ (void )span_catcher->GetSpans ();
949+
950+ // 3rd call: DeleteObject proves bucket metadata was NOT evicted by object
951+ // error
952+ del_req.set_object (" another-object" );
953+ auto res_del2 =
954+ actual->DeleteObject ({del_req, options}).then (expect_no_context);
955+ delete_p2.set_value (Status{});
956+ ASSERT_STATUS_OK (res_del2.get ());
957+
958+ auto spans = span_catcher->GetSpans ();
959+ EXPECT_THAT (
960+ spans,
961+ ElementsAre (AllOf (
962+ SpanNamed (" storage::AsyncConnection::DeleteObject" ),
963+ SpanWithStatus (opentelemetry::trace::StatusCode::kOk ),
964+ SpanHasAttributes (
965+ OTelAttribute<std::string>(" gcp.resource.destination.id" ,
966+ " projects/123456/buckets/test-bucket" ),
967+ OTelAttribute<std::string>(" gcp.resource.destination.location" ,
968+ " us-east1" )))));
969+ }
970+
766971} // namespace
767972GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
768973} // namespace storage_internal
0 commit comments