diff --git a/.bazelignore b/.bazelignore index 98112232070b2..51934bc1a7518 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,5 +1,7 @@ build-out/ -ci/ +ci/bzlmod_tests/consumer-test +ci/verify_current_targets +ci/verify_deprecated_targets cmake-out/ cmake-build-debug/ cmake-build-coverage/ diff --git a/ci/cloudbuild/builds/lib/integration.sh b/ci/cloudbuild/builds/lib/integration.sh index f58dedcfde548..af5595e8fff2e 100644 --- a/ci/cloudbuild/builds/lib/integration.sh +++ b/ci/cloudbuild/builds/lib/integration.sh @@ -29,9 +29,11 @@ source module ci/lib/io.sh # To run the integration tests we need to install the dependencies for the storage emulator export PATH="${HOME}/.local/bin:${PATH}" -python3 -m pip uninstall -y --quiet googleapis-storage-testbench -python3 -m pip install --upgrade --user --quiet --disable-pip-version-check \ - "git+https://github.com/googleapis/storage-testbench@v0.61.0" +if python3 -m pip --version >/dev/null 2>&1; then + python3 -m pip uninstall -y --quiet googleapis-storage-testbench || true + python3 -m pip install --upgrade --user --quiet --disable-pip-version-check \ + "git+https://github.com/googleapis/storage-testbench@v0.61.0" || true +fi # Some of the tests will need a valid roots.pem file. rm -f /dev/shm/roots.pem @@ -51,7 +53,7 @@ function integration::bazel_args() { # Integration tests are inherently flaky. Make up to three attempts to get the # test passing. - args+=(--flaky_test_attempts=3) + # args+=(--flaky_test_attempts=3) args+=( # Common settings diff --git a/ci/cloudbuild/builds/observability.sh b/ci/cloudbuild/builds/observability.sh new file mode 100755 index 0000000000000..5e6130822edbd --- /dev/null +++ b/ci/cloudbuild/builds/observability.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +source "$(dirname "$0")/../../lib/init.sh" +source module ci/cloudbuild/builds/lib/bazel.sh +source module ci/cloudbuild/builds/lib/cloudcxxrc.sh +source module ci/cloudbuild/builds/lib/integration.sh +source module ci/lib/io.sh + +export CC=clang +export CXX=clang++ + +mapfile -t args < <(bazel::common_args) +args+=("--//google/cloud/bigtable:enable_metrics=true") +mapfile -t integration_args < <(integration::bazel_args) + +io::log_h2 "Building OtelCollector targets and Bigtable Observability integration tests" +io::run bazel build "${args[@]}" \ + //ci/otel_collector:otel_collector_main \ + //google/cloud/bigtable/tests:observability_integration_test-default \ + //google/cloud/bigtable/tests:observability_integration_test-dynamic-pool + +io::log_h2 "Running Bigtable Observability Integration Tests against production endpoint" +io::run bazel test "${args[@]}" "${integration_args[@]}" \ + --cache_test_results="auto" --test_output=all --test_timeout=60 \ + -- //google/cloud/bigtable/tests:observability_integration_test-default \ + //google/cloud/bigtable/tests:observability_integration_test-dynamic-pool diff --git a/ci/cloudbuild/triggers/observability-ci.yaml b/ci/cloudbuild/triggers/observability-ci.yaml new file mode 100644 index 0000000000000..d1209f6d52e41 --- /dev/null +++ b/ci/cloudbuild/triggers/observability-ci.yaml @@ -0,0 +1,28 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +filename: ci/cloudbuild/cloudbuild.yaml +github: + name: google-cloud-cpp + owner: googleapis + push: + branch: main +includeBuildLogs: INCLUDE_BUILD_LOGS_WITH_STATUS +name: observability-ci +substitutions: + _BUILD_NAME: observability + _DISTRO: fedora-latest-bazel + _TRIGGER_TYPE: ci +tags: +- ci diff --git a/ci/cloudbuild/triggers/observability-pr.yaml b/ci/cloudbuild/triggers/observability-pr.yaml new file mode 100644 index 0000000000000..5ad3cad27a40f --- /dev/null +++ b/ci/cloudbuild/triggers/observability-pr.yaml @@ -0,0 +1,29 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +filename: ci/cloudbuild/cloudbuild.yaml +github: + name: google-cloud-cpp + owner: googleapis + pullRequest: + branch: main + commentControl: COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY +includeBuildLogs: INCLUDE_BUILD_LOGS_WITH_STATUS +name: observability-pr +substitutions: + _BUILD_NAME: observability + _DISTRO: fedora-latest-bazel + _TRIGGER_TYPE: pr +tags: +- pr diff --git a/ci/lib/io.sh b/ci/lib/io.sh index e6c3835cb136c..b4f0c9dd7a6e9 100644 --- a/ci/lib/io.sh +++ b/ci/lib/io.sh @@ -30,7 +30,7 @@ fi # include guard # Callers may use these IO_* variables directly, but should prefer to use the # logging functions below instead. For example, prefer `io::log_green "..."` # over `echo "${IO_COLOR_GREEN}...${IO_RESET}"`. -if [ -t 0 ] && command -v tput >/dev/null; then +if [ -t 0 ] && command -v tput >/dev/null && tput bold >/dev/null 2>&1; then IO_BOLD="$(tput bold)" IO_COLOR_RED="$(tput setaf 1)" IO_COLOR_GREEN="$(tput setaf 2)" diff --git a/ci/otel_collector/ARCHITECTURE.md b/ci/otel_collector/ARCHITECTURE.md new file mode 100644 index 0000000000000..10b6a149c99fe --- /dev/null +++ b/ci/otel_collector/ARCHITECTURE.md @@ -0,0 +1,247 @@ +# Architectural Design: Observability Collector Server + +## 1. Objective + +Provide a generalized design for an in-process and out-of-process fake gRPC +server to intercept, store, and verify OpenTelemetry / Cloud Monitoring and +Cloud Trace RPCs sent by any `google-cloud-cpp` client library (e.g., Bigtable, +Storage, Spanner, PubSub) during integration and system testing. + +______________________________________________________________________ + +## 2. Background & Rationale + +Multiple `google-cloud-cpp` client libraries export low-level transport and +client-side performance telemetry (metrics and traces) to Google Cloud +Observability backends via OpenTelemetry. While unit testing with in-process +mocks validates component logic, integration tests require verifying the +end-to-end telemetry pipeline across gRPC transports. + +By establishing a shared **Observability Collector Server** under internal CI +tooling infrastructure (`ci/otel_collector`): + +- **Separation of Customer vs Testing Code**: Keeps internal mock servers, test + binaries, and CI helper code inside `ci/` and out of customer-facing + header/library directories (`google/cloud/`). +- **Reusability Across All Libraries**: A single utility under `ci/` serves + Bigtable, Storage, Spanner, PubSub, and future SDK components exporting + telemetry. +- **Complete End-to-End Transport Coverage**: Verifies gRPC OpenTelemetry plugin + registration, batching readers/processors, protobuf serialization, and network + dispatch. +- **Process & Container Decoupled Testing**: Can be embedded into integration + test binaries or built as a standalone binary/container + (`ci/otel_collector_main`) for multi-language system testing. +- **Hermetic Test Execution**: Standardized verification RPCs enable resetting + server state between test cases to prevent telemetry pollution across tests. +- **Unified Observability Architecture**: Supports metric verification + (`GetRecordedMetrics`) and is architected to seamlessly accommodate trace + verification (`GetRecordedTraces`) in a single server framework. + +______________________________________________________________________ + +## 3. System Architecture + +The server hosts multiple gRPC service interfaces on a single listening port: + +1. **`google.monitoring.v3.MetricService`**: Standard Cloud Monitoring API + endpoint that receives `CreateTimeSeries` requests. +1. **`google.devtools.cloudtrace.v2.TraceService`**: Standard Cloud Trace API + endpoint receiving trace spans (future extension). +1. **`google.cloud.opentelemetry.testing.ObservabilityVerificationService`**: + Shared control-plane interface used by test suites to inspect and manage + captured metric and trace protobufs. + +### Architecture Overview + +```mermaid +graph TD + subgraph Test_Process ["Test Process Boundary"] + TestHarness["Integration Test Suite (Bigtable, Storage, Spanner, etc.)"] + SDKClient["Google Cloud C++ Client Library (Exporting telemetry via OpenTelemetry)"] + end + + subgraph Server_Process ["Observability Collector Server Process (ci/otel_collector)"] + MetricSvc["google.monitoring.v3.MetricService - CreateTimeSeries"] + VerifySvc["google.cloud.opentelemetry.testing.ObservabilityVerificationService"] + Storage["In-Memory Telemetry Request Store"] + end + + SDKClient -->|"1. Export telemetry over gRPC (GOOGLE_CLOUD_CPP_METRIC_SERVICE_ENDPOINT)"| MetricSvc + MetricSvc -->|"2. Record requests"| Storage + + TestHarness -->|"3. Clear state / Query recorded telemetry"| VerifySvc + VerifySvc -->|"4. Read recorded protobufs"| Storage + VerifySvc -->|"5. Return GetRecordedMetricsResponse"| TestHarness +``` + +______________________________________________________________________ + +## 4. Service Contracts & Protobuf Specification + +### 4.1 Imported Services + +Standard Google Cloud Observability proto definitions: + +- `google/monitoring/v3/metric_service.proto` +- `google/devtools/cloudtrace/v2/tracing.proto` (optional/future) + +### 4.2 Generic Control-Plane Proto Schema (`protos/google/cloud/opentelemetry/testing/observability_verification.proto`) + +```protobuf +syntax = "proto3"; + +package google.cloud.opentelemetry.testing; + +import "google/monitoring/v3/metric_service.proto"; +import "google/protobuf/empty.proto"; + +// Response containing all captured CreateTimeSeriesRequest protobuf messages. +message GetRecordedMetricsResponse { + repeated google.monitoring.v3.CreateTimeSeriesRequest requests = 1; +} + +// Control-plane service for test harnesses to query and manage captured observability telemetry. +service ObservabilityVerificationService { + // Returns all CreateTimeSeriesRequests captured since the last reset. + rpc GetRecordedMetrics(google.protobuf.Empty) + returns (GetRecordedMetricsResponse); + + // Resets the server metric state by clearing all recorded metric requests. + rpc ClearRecordedMetrics(google.protobuf.Empty) + returns (google.protobuf.Empty); +} +``` + +______________________________________________________________________ + +## 5. Sequence Diagram: Generic Integration Test Lifecycle + +```mermaid +sequenceDiagram + autonumber + participant Server as Observability Collector (ci/otel_collector) + participant Client as Google Cloud Client SDK + participant Test as Test Suite Harness + + Test->>Server: ClearRecordedMetrics() + Server-->>Test: OK (Empty) + + Test->>Client: Perform SDK Operations (API calls) + Note over Client: OpenTelemetry exporter gathers metrics and triggers periodic flush + Client->>Server: CreateTimeSeries(CreateTimeSeriesRequest) + Note over Server: Store request in thread-safe memory + Server-->>Client: OK (Empty) + + Note over Test: Wait for flush period (or force flush) + Test->>Server: GetRecordedMetrics() + Server-->>Test: GetRecordedMetricsResponse [requests...] + Note over Test: Inspect and validate metric.type, MonitoredResource, project_id, and point values +``` + +______________________________________________________________________ + +## 6. Implementation & Directory Layout + +### 6.1 Repository Directory Layout + +To keep internal test infrastructure strictly separate from customer-facing +headers and libraries: + +- **Protobuf Schema**: + `protos/google/cloud/opentelemetry/testing/observability_verification.proto` +- **C++ Implementation Header**: `ci/otel_collector/otel_collector.h` +- **C++ Implementation Source**: `ci/otel_collector/otel_collector.cc` +- **Standalone Server Binary Main**: `ci/otel_collector/otel_collector_main.cc` +- **Build Targets (CMake / Bazel)**: Internal target defined under + `ci/otel_collector/CMakeLists.txt` (not installed or exposed in public SDK + distribution packages). + +### 6.2 C++ Fake Server Implementation (`google::cloud::testing_util`) + +```cpp +#include "protos/google/cloud/opentelemetry/testing/observability_verification.grpc.pb.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace testing_util { + +class OtelCollectorServer final + : public google::monitoring::v3::MetricService::Service, + public google::cloud::opentelemetry::testing::ObservabilityVerificationService::Service { + public: + // --- MetricService Interface --- + grpc::Status CreateTimeSeries( + grpc::ServerContext* /*context*/, + google::monitoring::v3::CreateTimeSeriesRequest const* request, + google::protobuf::Empty* /*response*/) override { + std::lock_guard lock(mu_); + metric_requests_.push_back(*request); + return grpc::Status::OK; + } + + // --- ObservabilityVerificationService Interface --- + grpc::Status GetRecordedMetrics( + grpc::ServerContext* /*context*/, + google::protobuf::Empty const* /*request*/, + google::cloud::opentelemetry::testing::GetRecordedMetricsResponse* response) override { + std::lock_guard lock(mu_); + for (auto const& req : metric_requests_) { + *response->add_requests() = req; + } + return grpc::Status::OK; + } + + grpc::Status ClearRecordedMetrics( + grpc::ServerContext* /*context*/, + google::protobuf::Empty const* /*request*/, + google::protobuf::Empty* /*response*/) override { + std::lock_guard lock(mu_); + metric_requests_.clear(); + return grpc::Status::OK; + } + + private: + std::mutex mu_; + std::vector metric_requests_; +}; + +} // namespace testing_util +} // namespace cloud +} // namespace google +``` + +______________________________________________________________________ + +## 7. Generic Client Verification Matrix + +Integration tests across different libraries can reuse +`ObservabilityVerificationService` to validate service-specific observability +contracts: + +| Library | Sample Target Metric Types | Key MonitoredResource Labels to Assert | +| :----------- | :------------------------------------------------------------------------- | :------------------------------------------------------------- | +| **Bigtable** | `grpc.client.attempt.duration`, `grpc.subchannel.open_connections` | `project_id`, `instance`, `app_profile`, `client_name`, `uuid` | +| **Storage** | `storage.googleapis.com/client/throughput`, `grpc.client.attempt.duration` | `project_id`, `bucket_name`, `client_name` | +| **Spanner** | `spanner.googleapis.com/client/session_pool/active_count` | `project_id`, `instance_id`, `database_id` | +| **PubSub** | `pubsub.googleapis.com/client/published_message_count` | `project_id`, `topic_id` | + +______________________________________________________________________ + +## 8. Extensibility & Future Expansion + +1. **Trace Verification Integration**: The `ObservabilityVerificationService` + control plane can be extended with + `google.devtools.cloudtrace.v2.TraceService` implementation and corresponding + methods: + - `rpc GetRecordedTraces(google.protobuf.Empty) returns (GetRecordedTracesResponse)` + - `rpc ClearRecordedTraces(google.protobuf.Empty) returns (google.protobuf.Empty)` +1. **Error Injection API**: Add `SetCreateTimeSeriesStatus(grpc::Status status)` + to simulate transient backend failures and test SDK fallback/retry behavior. +1. **Reactive Verification**: Support streaming RPCs or server push + notifications when expected telemetry points are recorded to accelerate + integration test execution without arbitrary `sleep` timeouts. diff --git a/ci/otel_collector/BUILD.bazel b/ci/otel_collector/BUILD.bazel new file mode 100644 index 0000000000000..4f1adaf496ae2 --- /dev/null +++ b/ci/otel_collector/BUILD.bazel @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +cc_library( + name = "otel_collector", + srcs = ["otel_collector.cc"], + hdrs = ["otel_collector.h"], + deps = [ + "//protos/google/cloud/opentelemetry/testing:observability_verification_cc_grpc", + "//protos/google/cloud/opentelemetry/testing:observability_verification_cc_proto", + "@googleapis//google/monitoring/v3:monitoring_cc_grpc", + "@googleapis//google/monitoring/v3:monitoring_cc_proto", + "@grpc//:grpc++", + ], +) + +cc_binary( + name = "otel_collector_main", + srcs = ["otel_collector_main.cc"], + deps = [ + ":otel_collector", + "@grpc//:grpc++", + ], +) diff --git a/ci/otel_collector/CMakeLists.txt b/ci/otel_collector/CMakeLists.txt new file mode 100644 index 0000000000000..313ac861ead35 --- /dev/null +++ b/ci/otel_collector/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +if (NOT BUILD_TESTING) + return() +endif () + +add_library(otel_collector otel_collector.cc otel_collector.h) +target_link_libraries( + otel_collector PUBLIC google_cloud_cpp_opentelemetry_testing_protos + google-cloud-cpp::monitoring_protos gRPC::grpc++) +target_include_directories(otel_collector PUBLIC ${PROJECT_SOURCE_DIR}) +google_cloud_cpp_add_common_options(otel_collector NO_WARNINGS) + +google_cloud_cpp_add_executable(target "otel_collector_main" + "otel_collector_main.cc") +target_link_libraries(otel_collector_main PRIVATE otel_collector gRPC::grpc++) +google_cloud_cpp_add_common_options(otel_collector_main NO_WARNINGS) diff --git a/ci/otel_collector/otel_collector.cc b/ci/otel_collector/otel_collector.cc new file mode 100644 index 0000000000000..36251f17e467e --- /dev/null +++ b/ci/otel_collector/otel_collector.cc @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ci/otel_collector/otel_collector.h" + +namespace google { +namespace cloud { +namespace testing_util { + +grpc::Status OtelCollectorServer::CreateTimeSeries( + grpc::ServerContext* /*context*/, + google::monitoring::v3::CreateTimeSeriesRequest const* request, + google::protobuf::Empty* /*response*/) { + std::lock_guard lock(mu_); + metric_requests_.push_back(*request); + return grpc::Status::OK; +} + +grpc::Status OtelCollectorServer::CreateServiceTimeSeries( + grpc::ServerContext* /*context*/, + google::monitoring::v3::CreateTimeSeriesRequest const* request, + google::protobuf::Empty* /*response*/) { + std::lock_guard lock(mu_); + metric_requests_.push_back(*request); + return grpc::Status::OK; +} + +grpc::Status OtelCollectorServer::GetRecordedMetrics( + grpc::ServerContext* /*context*/, + google::protobuf::Empty const* /*request*/, + google::cloud::opentelemetry::testing::GetRecordedMetricsResponse* + response) { + std::lock_guard lock(mu_); + for (auto const& req : metric_requests_) { + *response->add_requests() = req; + } + return grpc::Status::OK; +} + +grpc::Status OtelCollectorServer::ClearRecordedMetrics( + grpc::ServerContext* /*context*/, + google::protobuf::Empty const* /*request*/, + google::protobuf::Empty* /*response*/) { + Clear(); + return grpc::Status::OK; +} + +std::vector +OtelCollectorServer::recorded_metrics() const { + std::lock_guard lock(mu_); + return metric_requests_; +} + +void OtelCollectorServer::Clear() { + std::lock_guard lock(mu_); + metric_requests_.clear(); +} + +} // namespace testing_util +} // namespace cloud +} // namespace google diff --git a/ci/otel_collector/otel_collector.h b/ci/otel_collector/otel_collector.h new file mode 100644 index 0000000000000..145c5b31fdd8a --- /dev/null +++ b/ci/otel_collector/otel_collector.h @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_CI_OTEL_COLLECTOR_OTEL_COLLECTOR_H +#define GOOGLE_CLOUD_CPP_CI_OTEL_COLLECTOR_OTEL_COLLECTOR_H + +#include "protos/google/cloud/opentelemetry/testing/observability_verification.grpc.pb.h" +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace testing_util { + +/** + * In-process or standalone test server that intercepts Google Cloud + * Observability RPCs (e.g. Cloud Monitoring MetricService) and allows + * integration test harnesses to inspect recorded request protobufs over gRPC. + */ +class OtelCollectorServer final + : public google::monitoring::v3::MetricService::Service, + public google::cloud::opentelemetry::testing:: + ObservabilityVerificationService::Service { + public: + OtelCollectorServer() = default; + + // --- google.monitoring.v3.MetricService Implementation --- + grpc::Status CreateTimeSeries( + grpc::ServerContext* context, + google::monitoring::v3::CreateTimeSeriesRequest const* request, + google::protobuf::Empty* response) override; + + grpc::Status CreateServiceTimeSeries( + grpc::ServerContext* context, + google::monitoring::v3::CreateTimeSeriesRequest const* request, + google::protobuf::Empty* response) override; + + // --- google.cloud.opentelemetry.testing.ObservabilityVerificationService + // Implementation --- + grpc::Status GetRecordedMetrics( + grpc::ServerContext* context, google::protobuf::Empty const* request, + google::cloud::opentelemetry::testing::GetRecordedMetricsResponse* + response) override; + + grpc::Status ClearRecordedMetrics(grpc::ServerContext* context, + google::protobuf::Empty const* request, + google::protobuf::Empty* response) override; + + // Direct C++ accessor for in-process testing without gRPC client overhead + std::vector + recorded_metrics() const; + + void Clear(); + + private: + mutable std::mutex mu_; + std::vector metric_requests_; +}; + +} // namespace testing_util +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_CI_OTEL_COLLECTOR_OTEL_COLLECTOR_H diff --git a/ci/otel_collector/otel_collector_main.cc b/ci/otel_collector/otel_collector_main.cc new file mode 100644 index 0000000000000..1080e61c0c1b9 --- /dev/null +++ b/ci/otel_collector/otel_collector_main.cc @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ci/otel_collector/otel_collector.h" +#include +#include +#include + +int main(int argc, char** argv) { + std::string server_address = "0.0.0.0:50051"; + if (argc > 1) { + server_address = "0.0.0.0:" + std::string(argv[1]); + } + + google::cloud::testing_util::OtelCollectorServer service; + + grpc::ServerBuilder builder; + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + builder.RegisterService( + static_cast(&service)); + builder.RegisterService( + static_cast(&service)); + + std::unique_ptr server = builder.BuildAndStart(); + if (!server) { + std::cerr << "Failed to start OtelCollectorServer on " << server_address + << std::endl; + return 1; + } + + std::cout << "Observability Collector Server listening on " << server_address + << std::endl; + server->Wait(); + return 0; +} diff --git a/google/cloud/bigtable/BUILD.bazel b/google/cloud/bigtable/BUILD.bazel index ba0cf3e9189d5..d0898645e708f 100644 --- a/google/cloud/bigtable/BUILD.bazel +++ b/google/cloud/bigtable/BUILD.bazel @@ -43,7 +43,10 @@ cc_library( srcs = google_cloud_cpp_bigtable_srcs, hdrs = google_cloud_cpp_bigtable_hdrs, local_defines = select({ - ":metrics_enabled": ["GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS"], + ":metrics_enabled": [ + "GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS", + "GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS", + ], "//conditions:default": [], }), visibility = [ @@ -65,6 +68,7 @@ cc_library( ] + select({ ":metrics_enabled": [ "//:opentelemetry", + "@grpc//:grpcpp_otel_plugin", "@opentelemetry-cpp//api", "@opentelemetry-cpp//sdk/src/metrics", ], @@ -121,6 +125,7 @@ cc_library( local_defines = select({ ":metrics_enabled": [ "GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS", + "GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS", "GOOGLE_CLOUD_CPP_BIGTABLE_QUERY_PLAN_REFRESH_ASSERT", ], "//conditions:default": ["GOOGLE_CLOUD_CPP_BIGTABLE_QUERY_PLAN_REFRESH_ASSERT"], diff --git a/google/cloud/bigtable/CMakeLists.txt b/google/cloud/bigtable/CMakeLists.txt index 2be88e34d3e0c..47fc356283896 100644 --- a/google/cloud/bigtable/CMakeLists.txt +++ b/google/cloud/bigtable/CMakeLists.txt @@ -190,6 +190,8 @@ add_library( internal/endpoint_options.h internal/google_bytes_traits.cc internal/google_bytes_traits.h + internal/grpc_metrics_exporter.cc + internal/grpc_metrics_exporter.h internal/logging_result_set_reader.cc internal/logging_result_set_reader.h internal/metrics.cc @@ -285,7 +287,17 @@ target_compile_definitions(google_cloud_cpp_bigtable PRIVATE GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS) target_link_libraries(google_cloud_cpp_bigtable PUBLIC google-cloud-cpp::opentelemetry) -set(EXTRA_MODULES "google_cloud_cpp_opentelemetry" "opentelemetry_metrics") +if (TARGET gRPC::grpcpp_otel_plugin) + target_compile_definitions( + google_cloud_cpp_bigtable + PRIVATE GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS) + target_link_libraries(google_cloud_cpp_bigtable + PUBLIC gRPC::grpcpp_otel_plugin) + set(EXTRA_MODULES "google_cloud_cpp_opentelemetry" "grpcpp_otel_plugin" + "opentelemetry_metrics") +else () + set(EXTRA_MODULES "google_cloud_cpp_opentelemetry" "opentelemetry_metrics") +endif () google_cloud_cpp_add_common_options(google_cloud_cpp_bigtable) target_include_directories( google_cloud_cpp_bigtable @@ -460,6 +472,7 @@ if (BUILD_TESTING) internal/defaults_test.cc internal/dynamic_channel_pool_test.cc internal/google_bytes_traits_test.cc + internal/grpc_metrics_exporter_test.cc internal/logging_result_set_reader_test.cc internal/metrics_test.cc internal/mutate_rows_limiter_test.cc @@ -528,6 +541,11 @@ if (BUILD_TESTING) target_compile_definitions( ${target} PRIVATE GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS) endif () + if (TARGET gRPC::grpcpp_otel_plugin) + target_compile_definitions( + ${target} + PRIVATE GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS) + endif () google_cloud_cpp_add_common_options(${target}) add_test(NAME ${target} COMMAND ${target}) endforeach () diff --git a/google/cloud/bigtable/bigtable_client_unit_tests.bzl b/google/cloud/bigtable/bigtable_client_unit_tests.bzl index 99c7af16cbaba..acb6542878348 100644 --- a/google/cloud/bigtable/bigtable_client_unit_tests.bzl +++ b/google/cloud/bigtable/bigtable_client_unit_tests.bzl @@ -55,6 +55,7 @@ bigtable_client_unit_tests = [ "internal/defaults_test.cc", "internal/dynamic_channel_pool_test.cc", "internal/google_bytes_traits_test.cc", + "internal/grpc_metrics_exporter_test.cc", "internal/logging_result_set_reader_test.cc", "internal/metrics_test.cc", "internal/mutate_rows_limiter_test.cc", diff --git a/google/cloud/bigtable/google_cloud_cpp_bigtable.bzl b/google/cloud/bigtable/google_cloud_cpp_bigtable.bzl index 3548270b00fa9..054d50d102fe4 100644 --- a/google/cloud/bigtable/google_cloud_cpp_bigtable.bzl +++ b/google/cloud/bigtable/google_cloud_cpp_bigtable.bzl @@ -94,6 +94,7 @@ google_cloud_cpp_bigtable_hdrs = [ "internal/dynamic_channel_pool.h", "internal/endpoint_options.h", "internal/google_bytes_traits.h", + "internal/grpc_metrics_exporter.h", "internal/logging_result_set_reader.h", "internal/metrics.h", "internal/mutate_rows_limiter.h", @@ -206,6 +207,7 @@ google_cloud_cpp_bigtable_srcs = [ "internal/default_row_reader.cc", "internal/defaults.cc", "internal/google_bytes_traits.cc", + "internal/grpc_metrics_exporter.cc", "internal/logging_result_set_reader.cc", "internal/metrics.cc", "internal/mutate_rows_limiter.cc", diff --git a/google/cloud/bigtable/internal/data_connection_impl.cc b/google/cloud/bigtable/internal/data_connection_impl.cc index 2d253b2a92dd8..631fd57013546 100644 --- a/google/cloud/bigtable/internal/data_connection_impl.cc +++ b/google/cloud/bigtable/internal/data_connection_impl.cc @@ -19,6 +19,7 @@ #include "google/cloud/bigtable/internal/bulk_mutator.h" #include "google/cloud/bigtable/internal/default_row_reader.h" #include "google/cloud/bigtable/internal/defaults.h" +#include "google/cloud/bigtable/internal/grpc_metrics_exporter.h" #include "google/cloud/bigtable/internal/logging_result_set_reader.h" #include "google/cloud/bigtable/internal/operation_context.h" #include "google/cloud/bigtable/internal/partial_result_set_reader.h" @@ -37,6 +38,7 @@ #include "google/cloud/idempotency.h" #include "google/cloud/internal/algorithm.h" #include "google/cloud/internal/async_retry_loop.h" +#include "google/cloud/internal/getenv.h" #include "google/cloud/internal/make_status.h" #include "google/cloud/internal/random.h" #include "google/cloud/internal/retry_loop.h" @@ -208,6 +210,12 @@ Options MetricsExporterConnectionOptions(Options options) { // to allow default Monitoring defaults. options.unset(); options.unset(); + auto endpoint = internal::GetEnv("GOOGLE_CLOUD_CPP_METRIC_SERVICE_ENDPOINT"); + // If we are using a local endpoint, we need to use insecure credentials. + if (endpoint && (endpoint->rfind("localhost:", 0) == 0 || + endpoint->rfind("127.0.0.1:", 0) == 0)) { + options.set(MakeInsecureCredentials()); + } return options; } #endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS @@ -252,6 +260,12 @@ DataConnectionImpl::DataConnectionImpl( auto gen = internal::MakeDefaultPRNG(); std::string client_uid = internal::Sample(gen, 16, "abcdefghijklmnopqrstuvwxyz0123456789"); +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + if (options_.has()) { + grpc_metrics_exporter_ = std::make_unique( + metric_service_connection_, options_, client_uid); + } +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS operation_context_factory_ = std::make_unique( std::move(client_uid), metric_service_connection_, options_); @@ -265,6 +279,8 @@ DataConnectionImpl::DataConnectionImpl( #endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_OTEL_METRICS } +DataConnectionImpl::~DataConnectionImpl() = default; + DataConnectionImpl::DataConnectionImpl( std::unique_ptr background, std::shared_ptr stub, diff --git a/google/cloud/bigtable/internal/data_connection_impl.h b/google/cloud/bigtable/internal/data_connection_impl.h index a859d5c938ace..d2034931fbdf3 100644 --- a/google/cloud/bigtable/internal/data_connection_impl.h +++ b/google/cloud/bigtable/internal/data_connection_impl.h @@ -41,6 +41,8 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END namespace bigtable_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +class GrpcMetricsExporter; + // TODO(#16216): Remove this option in favor of addind a member variable to // store the instances. struct InstanceChannelAffinityOption { @@ -52,7 +54,7 @@ bigtable::Row TransformReadModifyWriteRowResponse( class DataConnectionImpl : public bigtable::DataConnection { public: - ~DataConnectionImpl() override = default; + ~DataConnectionImpl() override; DataConnectionImpl(std::unique_ptr background, std::unique_ptr stub_manager, @@ -149,6 +151,7 @@ class DataConnectionImpl : public bigtable::DataConnection { std::unique_ptr stub_manager_; std::shared_ptr<::google::cloud::monitoring_v3::MetricServiceConnection> metric_service_connection_; + std::unique_ptr grpc_metrics_exporter_; std::unique_ptr operation_context_factory_; std::shared_ptr limiter_; Options options_; diff --git a/google/cloud/bigtable/internal/grpc_metrics_exporter.cc b/google/cloud/bigtable/internal/grpc_metrics_exporter.cc new file mode 100644 index 0000000000000..0f68fbaf2bab3 --- /dev/null +++ b/google/cloud/bigtable/internal/grpc_metrics_exporter.cc @@ -0,0 +1,293 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/bigtable/internal/grpc_metrics_exporter.h" +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS +#include "google/cloud/bigtable/options.h" +#include "google/cloud/bigtable/version.h" +#include "google/cloud/opentelemetry/internal/monitoring_exporter.h" +#include "google/cloud/grpc_options.h" +#include "google/cloud/internal/algorithm.h" +#include "google/cloud/log.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include +#include +#include +#if OPENTELEMETRY_VERSION_MAJOR > 1 || \ + (OPENTELEMETRY_VERSION_MAJOR == 1 && OPENTELEMETRY_VERSION_MINOR >= 10) +#include +#include +#include +#include +#include +#endif // OPENTELEMETRY_VERSION_MAJOR > 1 || (OPENTELEMETRY_VERSION_MAJOR == 1 + // && OPENTELEMETRY_VERSION_MINOR >= 10) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +namespace google { +namespace cloud { +namespace bigtable_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +GrpcMetricsExporterRegistry& GrpcMetricsExporterRegistry::Singleton() { + static auto* registry = new GrpcMetricsExporterRegistry; + return *registry; +} + +bool GrpcMetricsExporterRegistry::Register(std::string authority) { + std::unique_lock lk(mu_); + return known_authority_.insert(std::move(authority)).second; +} + +void GrpcMetricsExporterRegistry::Clear() { + std::unique_lock lk(mu_); + known_authority_.clear(); +} + +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +std::vector MakeLatencyHistogramBoundaries() { + static auto const kBoundaries = [] { + using dseconds = std::chrono::duration>; + std::vector boundaries; + auto boundary = std::chrono::milliseconds(0); + auto increment = std::chrono::milliseconds(2); + for (int i = 0; i != 50; ++i) { + boundaries.push_back( + std::chrono::duration_cast(boundary).count()); + boundary += increment; + } + increment = std::chrono::milliseconds(10); + for (int i = 0; i != 150 && boundary <= std::chrono::minutes(5); ++i) { + boundaries.push_back( + std::chrono::duration_cast(boundary).count()); + if (i != 0 && i % 10 == 0) increment *= 2; + boundary += increment; + } + return boundaries; + }(); + return kBoundaries; +} + +namespace { + +void AddHistogramView(opentelemetry::sdk::metrics::MeterProvider& provider, + std::vector boundaries, std::string const& name, + std::string const& unit) { + auto constexpr kGrpcMeterName = "grpc-c++"; + auto constexpr kGrpcSchema = ""; + + auto histogram_aggregation_config = std::make_unique< + opentelemetry::sdk::metrics::HistogramAggregationConfig>(); + histogram_aggregation_config->boundaries_ = std::move(boundaries); + auto aggregation_config = + std::shared_ptr( + std::move(histogram_aggregation_config)); + + auto description = absl::StrCat("A view of ", name, + " with histogram boundaries more appropriate " + "for Google Cloud Bigtable RPCs"); + +#if OPENTELEMETRY_VERSION_MAJOR > 1 || \ + (OPENTELEMETRY_VERSION_MAJOR == 1 && OPENTELEMETRY_VERSION_MINOR >= 23) + (void)unit; + provider.AddView( + opentelemetry::sdk::metrics::InstrumentSelectorFactory::Create( + opentelemetry::sdk::metrics::InstrumentType::kHistogram, name, unit), + opentelemetry::sdk::metrics::MeterSelectorFactory::Create( + kGrpcMeterName, grpc::Version(), kGrpcSchema), + opentelemetry::sdk::metrics::ViewFactory::Create( + name, std::move(description), + opentelemetry::sdk::metrics::AggregationType::kHistogram, + std::move(aggregation_config))); +#elif OPENTELEMETRY_VERSION_MAJOR > 1 || \ + (OPENTELEMETRY_VERSION_MAJOR == 1 && OPENTELEMETRY_VERSION_MINOR >= 10) + provider.AddView( + opentelemetry::sdk::metrics::InstrumentSelectorFactory::Create( + opentelemetry::sdk::metrics::InstrumentType::kHistogram, name, unit), + opentelemetry::sdk::metrics::MeterSelectorFactory::Create( + kGrpcMeterName, grpc::Version(), kGrpcSchema), + opentelemetry::sdk::metrics::ViewFactory::Create( + name, std::move(description), unit, + opentelemetry::sdk::metrics::AggregationType::kHistogram, + std::move(aggregation_config))); +#else // OPENTELEMETRY_VERSION_MAJOR > 1 || (OPENTELEMETRY_VERSION_MAJOR == 1 + // && OPENTELEMETRY_VERSION_MINOR >= 10) + (void)unit; + provider.AddView( + std::make_unique( + opentelemetry::sdk::metrics::InstrumentType::kHistogram, name), + std::make_unique( + kGrpcMeterName, grpc::Version(), kGrpcSchema), + std::make_unique( + name, std::move(description), + opentelemetry::sdk::metrics::AggregationType::kHistogram, + std::move(aggregation_config))); +#endif // OPENTELEMETRY_VERSION_MAJOR > 1 || (OPENTELEMETRY_VERSION_MAJOR == 1 + // && OPENTELEMETRY_VERSION_MINOR >= 23) +} + +} // namespace + +std::shared_ptr MakeGrpcMeterProvider( + std::unique_ptr exporter, + opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions + reader_options) { +#if OPENTELEMETRY_VERSION_MAJOR > 1 || \ + (OPENTELEMETRY_VERSION_MAJOR == 1 && OPENTELEMETRY_VERSION_MINOR >= 10) + auto provider = opentelemetry::sdk::metrics::MeterProviderFactory::Create( + std::make_unique(), + opentelemetry::sdk::resource::Resource::Create({})); +#else // OPENTELEMETRY_VERSION_MAJOR > 1 || (OPENTELEMETRY_VERSION_MAJOR == 1 + // && OPENTELEMETRY_VERSION_MINOR >= 10) + auto provider = std::make_unique( + std::make_unique(), + opentelemetry::sdk::resource::Resource::Create({})); +#endif // OPENTELEMETRY_VERSION_MAJOR > 1 || (OPENTELEMETRY_VERSION_MAJOR == 1 + // && OPENTELEMETRY_VERSION_MINOR >= 10) + auto* p = + static_cast(provider.get()); + AddHistogramView(*p, MakeLatencyHistogramBoundaries(), + "grpc.client.attempt.duration", "s"); + +#if OPENTELEMETRY_VERSION_MAJOR > 1 || OPENTELEMETRY_VERSION_MINOR >= 10 + p->AddMetricReader( + opentelemetry::sdk::metrics::PeriodicExportingMetricReaderFactory::Create( + std::move(exporter), std::move(reader_options))); +#else // OPENTELEMETRY_VERSION_MAJOR > 1 || OPENTELEMETRY_VERSION_MINOR >= 10 + p->AddMetricReader( + std::make_unique< + opentelemetry::sdk::metrics::PeriodicExportingMetricReader>( + std::move(exporter), std::move(reader_options))); +#endif // OPENTELEMETRY_VERSION_MAJOR > 1 || OPENTELEMETRY_VERSION_MINOR >= 10 + + return std::shared_ptr( + std::move(provider)); +} + +MonitoredResourceResult MakeMonitoredResource( + opentelemetry::sdk::metrics::PointDataAttributes const& pda, + Options const& options, std::string const& client_uid) { + google::api::MonitoredResource resource; + resource.set_type("bigtable.googleapis.com/Client"); + auto& labels = *resource.mutable_labels(); + auto const& attributes = pda.attributes.GetAttributes(); + auto get_attr = [&](std::string const& key) { + auto it = attributes.find(key); + if (it == attributes.end()) return std::string{}; + return opentelemetry::nostd::get(it->second); + }; + auto project_id = get_attr("project_id"); + labels["project_id"] = project_id; + labels["instance"] = get_attr("instance"); + labels["app_profile"] = options.get(); + labels["client_name"] = "cpp.Bigtable/" + bigtable::version_string(); + labels["uuid"] = client_uid; + return MonitoredResourceResult{std::move(project_id), std::move(resource)}; +} + +GrpcMetricsExporter::GrpcMetricsExporter( + std::shared_ptr const& conn, + Options const& options, std::string const& client_uid) { + auto authority = options.get(); + if (!GrpcMetricsExporterRegistry::Singleton().Register(authority)) return; + + auto dynamic_resource_fn = + [options, client_uid]( + opentelemetry::sdk::metrics::PointDataAttributes const& pda) { + auto res = MakeMonitoredResource(pda, options, client_uid); + return std::make_pair(std::move(res.project_id), + std::move(res.resource)); + }; + + std::set excluded_labels{"project_id", "instance"}; + auto resource_filter_fn = + [excluded_labels = std::move(excluded_labels)](std::string const& key) { + return internal::Contains(excluded_labels, key); + }; + + auto exporter = otel_internal::MakeMonitoringExporter( + dynamic_resource_fn, resource_filter_fn, conn, options); + + auto reader_options = + opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions{}; + // otel::PeriodicExportingMetricReader enforces that export_timeout_millis < + // export_interval_millis. + reader_options.export_interval_millis = + options.get(); + reader_options.export_timeout_millis = + (std::min)(std::chrono::milliseconds(std::chrono::seconds(30)), + reader_options.export_interval_millis / 2); + + provider_ = + MakeGrpcMeterProvider(std::move(exporter), std::move(reader_options)); + + auto const metrics = std::vector{ + absl::string_view{"grpc.client.attempt.duration"}, + absl::string_view{"grpc.lb.rls.default_target_picks"}, + absl::string_view{"grpc.lb.rls.target_picks"}, + absl::string_view{"grpc.lb.rls.failed_picks"}, + absl::string_view{"grpc.xds_client.server_failure"}, + absl::string_view{"grpc.xds_client.resource_updates_invalid"}, + absl::string_view{"grpc.subchannel.disconnections"}, + absl::string_view{"grpc.subchannel.connection_attempts_succeeded"}, + absl::string_view{"grpc.subchannel.connection_attempts_failed"}, + absl::string_view{"grpc.subchannel.open_connections"}, + }; + auto scope_filter = + [authority = std::move(authority)]( + grpc::OpenTelemetryPluginBuilder::ChannelScope const& scope) { + return scope.default_authority() == authority; + }; + auto status = + grpc::OpenTelemetryPluginBuilder() + .SetMeterProvider(provider_) + .EnableMetrics(metrics) + .SetGenericMethodAttributeFilter([](absl::string_view target) { + return absl::StartsWith(target, "google.bigtable.v2"); + }) + .SetChannelScopeFilter(std::move(scope_filter)) + .BuildAndRegisterGlobal(); + if (!status.ok()) { + GCP_LOG(ERROR) << "Cannot register provider status=" << status.ToString(); + } +} + +#else // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +GrpcMetricsExporter::GrpcMetricsExporter( + std::shared_ptr const&, + Options const&, std::string const&) {} + +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace bigtable_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/bigtable/internal/grpc_metrics_exporter.h b/google/cloud/bigtable/internal/grpc_metrics_exporter.h new file mode 100644 index 0000000000000..8886ff8499642 --- /dev/null +++ b/google/cloud/bigtable/internal/grpc_metrics_exporter.h @@ -0,0 +1,98 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_BIGTABLE_INTERNAL_GRPC_METRICS_EXPORTER_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_BIGTABLE_INTERNAL_GRPC_METRICS_EXPORTER_H + +#include "google/cloud/options.h" +#include "google/cloud/version.h" +#include +#include +#include +#include + +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS +#include "google/cloud/monitoring/v3/metric_connection.h" +#include "google/api/monitored_resource.pb.h" +#include +#include +#include +#include +#include +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +namespace google { +namespace cloud { +namespace monitoring_v3 { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +class MetricServiceConnection; +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace monitoring_v3 + +namespace bigtable_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +class GrpcMetricsExporterRegistry { + public: + static GrpcMetricsExporterRegistry& Singleton(); + + // Returns true if authority is newly registered, false if it was already + // registered. + bool Register(std::string authority); + + void Clear(); + + private: + GrpcMetricsExporterRegistry() = default; + + std::set known_authority_; + std::mutex mu_; +}; + +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS +struct MonitoredResourceResult { + std::string project_id; + google::api::MonitoredResource resource; +}; + +MonitoredResourceResult MakeMonitoredResource( + opentelemetry::sdk::metrics::PointDataAttributes const& pda, + Options const& options, std::string const& client_uid); + +std::vector MakeLatencyHistogramBoundaries(); + +std::shared_ptr MakeGrpcMeterProvider( + std::unique_ptr exporter, + opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions + reader_options); +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +class GrpcMetricsExporter { + public: + GrpcMetricsExporter( + std::shared_ptr const& conn, + Options const& options, std::string const& client_uid); + + private: +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + std::shared_ptr provider_; +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS +}; + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace bigtable_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_BIGTABLE_INTERNAL_GRPC_METRICS_EXPORTER_H diff --git a/google/cloud/bigtable/internal/grpc_metrics_exporter_test.cc b/google/cloud/bigtable/internal/grpc_metrics_exporter_test.cc new file mode 100644 index 0000000000000..9909cadeecd7e --- /dev/null +++ b/google/cloud/bigtable/internal/grpc_metrics_exporter_test.cc @@ -0,0 +1,334 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS + +#include "google/cloud/bigtable/internal/grpc_metrics_exporter.h" +#include "google/cloud/bigtable/options.h" +#include "google/cloud/bigtable/version.h" +#include "google/cloud/grpc_options.h" +#include "google/cloud/options.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace google { +namespace cloud { +namespace bigtable_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +using ::testing::AllOf; +using ::testing::AtLeast; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Eq; +using ::testing::Ge; +using ::testing::IsEmpty; +using ::testing::Le; +using ::testing::Not; +using ::testing::ResultOf; +using ::testing::Return; +using ::testing::SizeIs; +using ::testing::VariantWith; + +class MockPushMetricExporter + : public opentelemetry::sdk::metrics::PushMetricExporter { + public: + // NOLINTBEGIN(bugprone-exception-escape) + MOCK_METHOD(opentelemetry::sdk::common::ExportResult, Export, + (opentelemetry::sdk::metrics::ResourceMetrics const&), + (noexcept, override)); + + MOCK_METHOD(opentelemetry::sdk::metrics::AggregationTemporality, + GetAggregationTemporality, + (opentelemetry::sdk::metrics::InstrumentType), + (const, noexcept, override)); + + MOCK_METHOD(bool, ForceFlush, (std::chrono::microseconds), + (noexcept, override)); + MOCK_METHOD(bool, Shutdown, (std::chrono::microseconds), + (noexcept, override)); + // NOLINTEND(bugprone-exception-escape) +}; + +auto constexpr kDurationMetric = "grpc.client.attempt.duration"; + +auto MatchesLatencyBoundaries() { + return ResultOf( + "boundaries are latency boundaries", + [](opentelemetry::sdk::metrics::HistogramPointData const& hpd) { + return hpd.boundaries_; + }, + ElementsAreArray(MakeLatencyHistogramBoundaries())); +} + +auto ExpectedLatencyHistogram() { + return ResultOf( + "data is histogram with right boundaries", + [](opentelemetry::sdk::metrics::PointDataAttributes const& pda) { + return pda.point_data; + }, + VariantWith( + MatchesLatencyBoundaries())); +} + +template +auto WithPointData(Matcher&& matcher) { + return ResultOf( + "instrument descriptor name", + [](opentelemetry::sdk::metrics::MetricData const& md) { + return md.point_data_attr_; + }, + std::forward(matcher)); +} + +template +auto WithMetricsData(Matcher&& matcher) { + return ResultOf( + "metric_data_", + [](opentelemetry::sdk::metrics::ScopeMetrics const& sm) { + return sm.metric_data_; + }, + std::forward(matcher)); +} + +auto MatchesInstrumentName(std::string name) { + return ResultOf( + "instrument descriptor name", + [](opentelemetry::sdk::metrics::MetricData const& md) { + return md.instrument_descriptor.name_; + }, + std::move(name)); +} + +auto MetricDataEmpty() { + return ResultOf( + "scope_metric_data_", + [](opentelemetry::sdk::metrics::ResourceMetrics const& data) { + return data.scope_metric_data_; + }, + IsEmpty()); +} + +auto MetricDataNotEmpty() { + return ResultOf( + "scope_metric_data_", + [](opentelemetry::sdk::metrics::ResourceMetrics const& data) { + return data.scope_metric_data_; + }, + Not(IsEmpty())); +} + +auto constexpr kExportInterval = std::chrono::milliseconds(50); +auto constexpr kExportTimeout = std::chrono::milliseconds(25); + +auto TestReaderOptions() { + opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions + reader_options; + reader_options.export_interval_millis = kExportInterval; + reader_options.export_timeout_millis = kExportTimeout; + return reader_options; +} + +TEST(GrpcMetricsExporterRegistryTest, SingletonAndClear) { + auto& registry = GrpcMetricsExporterRegistry::Singleton(); + registry.Clear(); + + EXPECT_TRUE(registry.Register("test-authority-1")); + EXPECT_FALSE(registry.Register("test-authority-1")); + EXPECT_TRUE(registry.Register("test-authority-2")); + + registry.Clear(); + EXPECT_TRUE(registry.Register("test-authority-1")); +} + +TEST(GrpcMetricsExporterTest, MakeMonitoredResource) { + Options options; + options.set("test-app-profile"); + std::string const client_uid = "test-client-uid"; + + opentelemetry::sdk::metrics::PointDataAttributes pda; + pda.attributes = opentelemetry::sdk::metrics::PointAttributes({ + {"project_id", "test-project"}, + {"instance", "test-instance"}, + }); + + auto result = MakeMonitoredResource(pda, options, client_uid); + EXPECT_THAT(result.project_id, Eq("test-project")); + + auto const& resource = result.resource; + EXPECT_THAT(resource.type(), Eq("bigtable.googleapis.com/Client")); + + auto const& labels = resource.labels(); + EXPECT_THAT(labels.at("project_id"), Eq("test-project")); + EXPECT_THAT(labels.at("instance"), Eq("test-instance")); + EXPECT_THAT(labels.at("app_profile"), Eq("test-app-profile")); + EXPECT_THAT(labels.at("client_name"), + Eq("cpp.Bigtable/" + bigtable::version_string())); + EXPECT_THAT(labels.at("uuid"), Eq("test-client-uid")); +} + +TEST(GrpcMetricsExporterTest, MakeMonitoredResourceMissingAttributes) { + Options options; + std::string const client_uid = "test-client-uid"; + + opentelemetry::sdk::metrics::PointDataAttributes pda; + pda.attributes = opentelemetry::sdk::metrics::PointAttributes({}); + + auto result = MakeMonitoredResource(pda, options, client_uid); + EXPECT_THAT(result.project_id, Eq("")); + + auto const& resource = result.resource; + EXPECT_THAT(resource.type(), Eq("bigtable.googleapis.com/Client")); + + auto const& labels = resource.labels(); + EXPECT_THAT(labels.at("project_id"), Eq("")); + EXPECT_THAT(labels.at("instance"), Eq("")); + EXPECT_THAT(labels.at("app_profile"), Eq("")); + EXPECT_THAT(labels.at("client_name"), + Eq("cpp.Bigtable/" + bigtable::version_string())); + EXPECT_THAT(labels.at("uuid"), Eq("test-client-uid")); +} + +TEST(GrpcMetricsExporterTest, MakeMonitoredResourcePartialAttributes) { + Options options; + std::string const client_uid = "test-client-uid"; + + opentelemetry::sdk::metrics::PointDataAttributes pda; + pda.attributes = opentelemetry::sdk::metrics::PointAttributes({ + {"project_id", "test-project"}, + }); + + auto result = MakeMonitoredResource(pda, options, client_uid); + EXPECT_THAT(result.project_id, Eq("test-project")); + + auto const& resource = result.resource; + EXPECT_THAT(resource.type(), Eq("bigtable.googleapis.com/Client")); + + auto const& labels = resource.labels(); + EXPECT_THAT(labels.at("project_id"), Eq("test-project")); + EXPECT_THAT(labels.at("instance"), Eq("")); + EXPECT_THAT(labels.at("app_profile"), Eq("")); + EXPECT_THAT(labels.at("client_name"), + Eq("cpp.Bigtable/" + bigtable::version_string())); + EXPECT_THAT(labels.at("uuid"), Eq("test-client-uid")); +} + +TEST(GrpcMetricsExporterTest, MakeMonitoredResourceExtraAttributes) { + Options options; + std::string const client_uid = "test-client-uid"; + + opentelemetry::sdk::metrics::PointDataAttributes pda; + pda.attributes = opentelemetry::sdk::metrics::PointAttributes({ + {"project_id", "test-project"}, + {"instance", "test-instance"}, + {"grpc.method", "google.bigtable.v2.Bigtable/ReadRows"}, + {"grpc.status", "OK"}, + }); + + auto result = MakeMonitoredResource(pda, options, client_uid); + EXPECT_THAT(result.project_id, Eq("test-project")); + + auto const& resource = result.resource; + EXPECT_THAT(resource.type(), Eq("bigtable.googleapis.com/Client")); + + auto const& labels = resource.labels(); + EXPECT_THAT(labels.size(), Eq(5)); + EXPECT_THAT(labels.at("project_id"), Eq("test-project")); + EXPECT_THAT(labels.at("instance"), Eq("test-instance")); + EXPECT_THAT(labels.at("app_profile"), Eq("")); + EXPECT_THAT(labels.at("client_name"), + Eq("cpp.Bigtable/" + bigtable::version_string())); + EXPECT_THAT(labels.at("uuid"), Eq("test-client-uid")); + EXPECT_THAT(labels.find("grpc.method"), Eq(labels.end())); + EXPECT_THAT(labels.find("grpc.status"), Eq(labels.end())); +} + +TEST(GrpcMetricsExporterTest, MakeLatencyHistogramBoundaries) { + auto const boundaries = MakeLatencyHistogramBoundaries(); + ASSERT_THAT(boundaries, Not(IsEmpty())); + ASSERT_THAT(boundaries, SizeIs(Le(200U))); + auto sorted = boundaries; + std::sort(sorted.begin(), sorted.end()); + EXPECT_THAT(boundaries, ElementsAreArray(sorted.begin(), sorted.end())); + std::vector diff; + std::adjacent_difference(boundaries.begin(), boundaries.end(), + std::back_inserter(diff)); + ASSERT_THAT(diff, Not(IsEmpty())); + EXPECT_THAT(*std::min_element(std::next(diff.begin()), diff.end()), + Ge(0.001)); + ASSERT_THAT(boundaries.back(), Le(300)); +} + +TEST(GrpcMetricsExporterTest, ValidateGrpcClientAttemptDuration) { + std::atomic export_count{0}; + auto mock = std::make_unique(); + EXPECT_CALL(*mock, Shutdown).WillOnce(Return(true)); + EXPECT_CALL(*mock, GetAggregationTemporality) + .WillRepeatedly(Return( + opentelemetry::sdk::metrics::AggregationTemporality::kCumulative)); + EXPECT_CALL(*mock, Export(MetricDataEmpty())) + .WillRepeatedly( + Return(opentelemetry::sdk::common::ExportResult::kSuccess)); + EXPECT_CALL(*mock, Export(MetricDataNotEmpty())) + .Times(AtLeast(1)) + .WillRepeatedly( + [&export_count]( + opentelemetry::sdk::metrics::ResourceMetrics const& data) { + EXPECT_THAT( + data.scope_metric_data_, + Contains(WithMetricsData(Contains(AllOf( + MatchesInstrumentName(kDurationMetric), + WithPointData(ElementsAre(ExpectedLatencyHistogram()))))))); + ++export_count; + return opentelemetry::sdk::common::ExportResult::kSuccess; + }); + + { + auto provider = MakeGrpcMeterProvider(std::move(mock), TestReaderOptions()); + auto meter = provider->GetMeter("grpc-c++", grpc::Version()); + auto histogram = meter->CreateDoubleHistogram(kDurationMetric, + "test-only-description", "s"); + for (int i = 0; i != 50 && export_count.load() == 0; ++i) { + histogram->Record(1.0, opentelemetry::context::Context{}); + std::this_thread::sleep_for(kExportInterval); + } + } +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace bigtable_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_BIGTABLE_WITH_GRPC_OTEL_METRICS diff --git a/google/cloud/bigtable/internal/operation_context_factory.cc b/google/cloud/bigtable/internal/operation_context_factory.cc index bd020586a8f95..fe563eb6d7c86 100644 --- a/google/cloud/bigtable/internal/operation_context_factory.cc +++ b/google/cloud/bigtable/internal/operation_context_factory.cc @@ -237,9 +237,13 @@ void MetricsOperationContextFactory::InitializeProvider( auto reader_options = opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions{}; - reader_options.export_timeout_millis = std::chrono::seconds(30); + // otel::PeriodicExportingMetricReader enforces that export_timeout_millis < + // export_interval_millis. reader_options.export_interval_millis = options.get(); + reader_options.export_timeout_millis = + (std::min)(std::chrono::milliseconds(std::chrono::seconds(30)), + reader_options.export_interval_millis / 2); options.set(true) .set( diff --git a/google/cloud/bigtable/tests/BUILD.bazel b/google/cloud/bigtable/tests/BUILD.bazel index c68ea6994fdd4..7325ddacb446f 100644 --- a/google/cloud/bigtable/tests/BUILD.bazel +++ b/google/cloud/bigtable/tests/BUILD.bazel @@ -39,6 +39,7 @@ VARIATIONS = { "//:bigtable", "//:common", "//:grpc_utils", + "//ci/otel_collector", "//google/cloud/bigtable:bigtable_client_testing", "//google/cloud/testing_util:google_cloud_cpp_testing_private", "@googletest//:gtest_main", diff --git a/google/cloud/bigtable/tests/bigtable_client_integration_tests.bzl b/google/cloud/bigtable/tests/bigtable_client_integration_tests.bzl index 907e01e0933dc..763abed29adeb 100644 --- a/google/cloud/bigtable/tests/bigtable_client_integration_tests.bzl +++ b/google/cloud/bigtable/tests/bigtable_client_integration_tests.bzl @@ -22,6 +22,7 @@ bigtable_client_integration_tests = [ "filters_integration_test.cc", "instance_admin_integration_test.cc", "mutations_integration_test.cc", + "observability_integration_test.cc", "table_admin_backup_integration_test.cc", "table_admin_iam_policy_integration_test.cc", "table_admin_integration_test.cc", diff --git a/google/cloud/bigtable/tests/data_integration_test.cc b/google/cloud/bigtable/tests/data_integration_test.cc index c190e58b20224..a6a75df535aca 100644 --- a/google/cloud/bigtable/tests/data_integration_test.cc +++ b/google/cloud/bigtable/tests/data_integration_test.cc @@ -578,8 +578,9 @@ TEST_F(DataIntegrationTest, TableApplyWithLogging) { auto const table_id = testing::TableTestEnvironment::table_id(); // Make a `Table` with an implementation that depends on the test's value - // parameter. - auto make_table = [&](Options const& options) { + // parameter. Disable metrics to avoid polluting the log file. + auto make_table = [&](Options options) { + options.set(false); auto conn = MakeDataConnection( {InstanceResource(Project(project_id()), instance_id())}, options); return Table(std::move(conn), diff --git a/google/cloud/bigtable/tests/observability_integration_test.cc b/google/cloud/bigtable/tests/observability_integration_test.cc new file mode 100644 index 0000000000000..35ae770f77df1 --- /dev/null +++ b/google/cloud/bigtable/tests/observability_integration_test.cc @@ -0,0 +1,166 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/bigtable/options.h" +#include "google/cloud/bigtable/testing/table_integration_test.h" +#include "google/cloud/credentials.h" +#include "google/cloud/testing_util/scoped_environment.h" +#include "google/cloud/testing_util/status_matchers.h" +#include "ci/otel_collector/otel_collector.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace bigtable { +namespace testing { +namespace { + +using ::google::cloud::bigtable::testing::TableTestEnvironment; +using ::google::cloud::testing_util::ScopedEnvironment; + +class ObservabilityIntegrationTest + : public ::google::cloud::bigtable::testing::TableIntegrationTest { + protected: + void SetUp() override { + TableIntegrationTest::SetUp(); + int port = 0; + grpc::ServerBuilder builder; + builder.AddListeningPort("localhost:0", grpc::InsecureServerCredentials(), + &port); + builder.RegisterService( + static_cast( + &collector_service_)); + builder.RegisterService( + static_cast<::google::cloud::opentelemetry::testing:: + ObservabilityVerificationService::Service*>( + &collector_service_)); + server_ = builder.BuildAndStart(); + server_address_ = "localhost:" + std::to_string(port); + } + + void TearDown() override { + if (server_) { + server_->Shutdown(); + server_->Wait(); + } + TableIntegrationTest::TearDown(); + } + + google::cloud::testing_util::OtelCollectorServer collector_service_; + std::unique_ptr server_; + std::string server_address_; +}; + +/// Use Table::Apply() to insert a single row. +void Apply(Table& table, std::string const& row_key, + std::vector const& cells) { + auto mutation = SingleRowMutation(row_key); + for (auto const& cell : cells) { + mutation.emplace_back( + SetCell(cell.family_name(), cell.column_qualifier(), + std::chrono::duration_cast( + std::chrono::microseconds(cell.timestamp())), + cell.value())); + } + auto status = table.Apply(std::move(mutation)); + ASSERT_STATUS_OK(status); +} + +TEST_F(ObservabilityIntegrationTest, VerifyOperationAndAttemptMetrics) { + if (UsingCloudBigtableEmulator()) { + GTEST_SKIP() << "Metrics export integration test runs against production"; + } + + // Redirect Cloud Monitoring metric export to local otel_collector + ScopedEnvironment env("GOOGLE_CLOUD_CPP_METRIC_SERVICE_ENDPOINT", + server_address_); + + // Set MetricsPeriodOption to 5s (minimum allowed by DefaultOptions; smaller + // periods reset to 60s) + auto options = + Options{}.set(true).set( + std::chrono::seconds(5)); + + auto const table_id = TableTestEnvironment::table_id(); + + // Add scoped connection to ensure metrics are flushed on destruction. + { + auto conn = MakeDataConnection( + {InstanceResource(Project(project_id()), instance_id())}, options); + auto table = Table(std::move(conn), + TableResource(project_id(), instance_id(), table_id)); + + std::string const row_key = "observability-row-1"; + std::vector expected{{row_key, "family4", "c0", 1000, "v1000"}, + {row_key, "family4", "c1", 2000, "v2000"}}; + + // Perform mutations and read calls + Apply(table, row_key, expected); + auto actual = ReadRows(table, Filter::PassAllFilter()); + CheckEqualUnordered(expected, actual); + + // Wait for the periodic 5-second exporter background thread to flush + // metrics while conn is active + std::this_thread::sleep_for(std::chrono::seconds(6)); + } + + auto recorded = collector_service_.recorded_metrics(); + ASSERT_FALSE(recorded.empty()); + + bool found_operation_latencies = false; + bool found_attempt_latencies = false; + + for (auto const& req : recorded) { + EXPECT_EQ(req.name(), "projects/" + project_id()); + + for (auto const& ts : req.time_series()) { + auto const& metric_type = ts.metric().type(); + + if (metric_type.find("operation_latencies") != std::string::npos) { + found_operation_latencies = true; + } + if (metric_type.find("attempt_latencies") != std::string::npos) { + found_attempt_latencies = true; + } + + auto const& labels = ts.resource().labels(); + auto project_it = labels.find("project_id"); + if (project_it != labels.end()) { + EXPECT_EQ(project_it->second, project_id()); + } + auto instance_it = labels.find("instance"); + if (instance_it != labels.end()) { + EXPECT_EQ(instance_it->second, instance_id()); + } + } + } + + EXPECT_TRUE(found_operation_latencies); + EXPECT_TRUE(found_attempt_latencies); +} + +} // namespace +} // namespace testing +} // namespace bigtable +} // namespace cloud +} // namespace google + +int main(int argc, char* argv[]) { + ::testing::InitGoogleMock(&argc, argv); + (void)::testing::AddGlobalTestEnvironment( + new ::google::cloud::bigtable::testing::TableTestEnvironment); + return RUN_ALL_TESTS(); +} diff --git a/protos/google/cloud/opentelemetry/BUILD.bazel b/protos/google/cloud/opentelemetry/BUILD.bazel new file mode 100644 index 0000000000000..6612426c930a5 --- /dev/null +++ b/protos/google/cloud/opentelemetry/BUILD.bazel @@ -0,0 +1,17 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 diff --git a/protos/google/cloud/opentelemetry/CMakeLists.txt b/protos/google/cloud/opentelemetry/CMakeLists.txt new file mode 100644 index 0000000000000..9de6f89d0cb2e --- /dev/null +++ b/protos/google/cloud/opentelemetry/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +if (NOT BUILD_TESTING) + return() +endif () + +if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/testing") + add_subdirectory(testing) +endif () diff --git a/protos/google/cloud/opentelemetry/testing/BUILD.bazel b/protos/google/cloud/opentelemetry/testing/BUILD.bazel new file mode 100644 index 0000000000000..6eb34bc466f9b --- /dev/null +++ b/protos/google/cloud/opentelemetry/testing/BUILD.bazel @@ -0,0 +1,44 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +proto_library( + name = "observability_verification_proto", + srcs = ["observability_verification.proto"], + deps = [ + "@com_google_protobuf//:empty_proto", + "@googleapis//google/monitoring/v3:monitoring_proto", + ], +) + +cc_proto_library( + name = "observability_verification_cc_proto", + visibility = ["//visibility:public"], + deps = [":observability_verification_proto"], +) + +cc_grpc_library( + name = "observability_verification_cc_grpc", + srcs = [":observability_verification_proto"], + grpc_only = True, + visibility = ["//visibility:public"], + deps = [":observability_verification_cc_proto"], +) diff --git a/protos/google/cloud/opentelemetry/testing/CMakeLists.txt b/protos/google/cloud/opentelemetry/testing/CMakeLists.txt new file mode 100644 index 0000000000000..e5510e1e5c55e --- /dev/null +++ b/protos/google/cloud/opentelemetry/testing/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +if (NOT BUILD_TESTING) + return() +endif () + +include(GoogleCloudCppCommon) +find_package(Protobuf CONFIG QUIET) +if (NOT Protobuf_FOUND) + find_package(Protobuf REQUIRED) +endif () + +include(CompileProtos) +google_cloud_cpp_find_proto_include_dir(PROTO_INCLUDE_DIR) + +google_cloud_cpp_grpcpp_library( + google_cloud_cpp_opentelemetry_testing_protos + observability_verification.proto + PROTO_PATH_DIRECTORIES + ${PROTO_INCLUDE_DIR} + ${PROJECT_SOURCE_DIR} + ${PROJECT_BINARY_DIR}/external/googleapis/src/googleapis_download + ${PROJECT_SOURCE_DIR}/protos/google/cloud/opentelemetry/testing) +target_link_libraries( + google_cloud_cpp_opentelemetry_testing_protos + PUBLIC google-cloud-cpp::api_client_protos + google-cloud-cpp::monitoring_protos + google-cloud-cpp::rpc_status_protos) +target_include_directories(google_cloud_cpp_opentelemetry_testing_protos SYSTEM + PUBLIC "${PROJECT_BINARY_DIR}/protos") +google_cloud_cpp_add_common_options( + google_cloud_cpp_opentelemetry_testing_protos NO_WARNINGS) +set_target_properties(google_cloud_cpp_opentelemetry_testing_protos + PROPERTIES CXX_CLANG_TIDY "") diff --git a/protos/google/cloud/opentelemetry/testing/observability_verification.proto b/protos/google/cloud/opentelemetry/testing/observability_verification.proto new file mode 100644 index 0000000000000..d4d30e03dac6e --- /dev/null +++ b/protos/google/cloud/opentelemetry/testing/observability_verification.proto @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.cloud.opentelemetry.testing; + +import "google/monitoring/v3/metric_service.proto"; +import "google/protobuf/empty.proto"; + +// Response containing all captured CreateTimeSeriesRequest protobuf messages. +message GetRecordedMetricsResponse { + repeated google.monitoring.v3.CreateTimeSeriesRequest requests = 1; +} + +// Control-plane service for test harnesses to query and manage captured +// observability telemetry. +service ObservabilityVerificationService { + // Returns all CreateTimeSeriesRequests captured since the last reset. + rpc GetRecordedMetrics(google.protobuf.Empty) + returns (GetRecordedMetricsResponse); + + // Resets the server metric state by clearing all recorded metric requests. + rpc ClearRecordedMetrics(google.protobuf.Empty) + returns (google.protobuf.Empty); +}