Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"apis/Google.Cloud.BinaryAuthorization.V1/Google.Cloud.BinaryAuthorization.V1/**",
"apis/Google.Cloud.BinaryAuthorization.V1Beta1/Google.Cloud.BinaryAuthorization.V1Beta1/**",
"apis/Google.Cloud.CapacityPlanner.V1Beta/Google.Cloud.CapacityPlanner.V1Beta/**",
"apis/Google.Cloud.BuiltInMetrics/Google.Cloud.BuiltInMetrics/**",
"apis/Google.Cloud.CertificateManager.V1/Google.Cloud.CertificateManager.V1/**",
"apis/Google.Cloud.Ces.V1/Google.Cloud.Ces.V1/**",
"apis/Google.Cloud.Ces.V1Beta/Google.Cloud.Ces.V1Beta/**",
Expand Down
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<!-- Support libraries (rarely referenced directly) -->
<PackageVersion Include="Google.Apis" Version="[1.73.0, 2.0.0)" />
<PackageVersion Include="Google.Apis.Auth" Version="[1.73.0, 2.0.0)" />

<!--
- The Google.Protobuf version should usually be the one that
Expand Down Expand Up @@ -69,5 +70,7 @@
<PackageVersion Include="NLog" Version="5.5.1" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.83.0" />
<PackageVersion Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.12" />
<PackageVersion Include="OpenTelemetry" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Each package name links to the documentation for that package.
| [Google.Cloud.BinaryAuthorization.V1](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.BinaryAuthorization.V1/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.BinaryAuthorization.V1)](https://nuget.org/packages/Google.Cloud.BinaryAuthorization.V1) | [Binary Authorization (V1 API)](https://cloud.google.com/binary-authorization/docs/reference/rpc) |
| [Google.Cloud.BinaryAuthorization.V1Beta1](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.BinaryAuthorization.V1Beta1/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.BinaryAuthorization.V1Beta1)](https://nuget.org/packages/Google.Cloud.BinaryAuthorization.V1Beta1) | [Binary Authorization (V1Beta1 API)](https://cloud.google.com/binary-authorization/docs/reference/rpc) |
| [Google.Cloud.CapacityPlanner.V1Beta](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.CapacityPlanner.V1Beta/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.CapacityPlanner.V1Beta)](https://nuget.org/packages/Google.Cloud.CapacityPlanner.V1Beta) | [Capacity Planner](https://cloud.google.com/capacity-planner/docs) |
| [Google.Cloud.BuiltInMetrics](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.BuiltInMetrics/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.BuiltInMetrics)](https://nuget.org/packages/Google.Cloud.BuiltInMetrics) | OpenTelemetry exporter and setup for Google Cloud client built-in metrics |
| [Google.Cloud.CertificateManager.V1](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.CertificateManager.V1/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.CertificateManager.V1)](https://nuget.org/packages/Google.Cloud.CertificateManager.V1) | [Certificate Manager](https://cloud.google.com/certificate-manager/docs) |
| [Google.Cloud.Ces.V1](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.Ces.V1/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.Ces.V1)](https://nuget.org/packages/Google.Cloud.Ces.V1) | [Gemini Enterprise for Customer Experience (V1 API)](https://docs.cloud.google.com/customer-engagement-ai/conversational-agents/ps) |
| [Google.Cloud.Ces.V1Beta](https://cloud.google.com/dotnet/docs/reference/Google.Cloud.Ces.V1Beta/latest) | [![NuGet](https://img.shields.io/nuget/v/Google.Cloud.Ces.V1Beta)](https://nuget.org/packages/Google.Cloud.Ces.V1Beta) | [Gemini Enterprise for Customer Experience (V1Beta API)](https://docs.cloud.google.com/customer-engagement-ai/conversational-agents/ps) |
Expand Down
7 changes: 7 additions & 0 deletions apis/Google.Cloud.BuiltInMetrics/.repo-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"distribution_name": "Google.Cloud.BuiltInMetrics",
"release_level": "preview",
"client_documentation": "https://cloud.google.com/dotnet/docs/reference/Google.Cloud.BuiltInMetrics/latest",
"library_type": "OTHER",
"language": "dotnet"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net462</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);1701;1702;1705;xUnit2004;xUnit2013</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Api.Gax.Testing" />
<ProjectReference Include="..\Google.Cloud.BuiltInMetrics\Google.Cloud.BuiltInMetrics.csproj" />
<ProjectReference Include="..\..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj" />
<PackageReference Include="Google.Cloud.Monitoring.V3" VersionOverride="[3.10.0, 4.0.0)" />
<ProjectReference Include="..\..\Google.Cloud.Spanner.Common.V1\Google.Cloud.Spanner.Common.V1\Google.Cloud.Spanner.Common.V1.csproj" />
<ProjectReference Include="..\..\Google.Cloud.Spanner.V1\Google.Cloud.Spanner.V1\Google.Cloud.Spanner.V1.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="Google.Cloud.BuiltInMetrics.IntegrationTests.csproj.google" Condition="Exists('Google.Cloud.BuiltInMetrics.IntegrationTests.csproj.google')" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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.

using Google.Apis.Auth.OAuth2;
using Google.Cloud.ClientTesting;
using Google.Cloud.Monitoring.V3;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.BuiltInMetrics.IntegrationTests;

public class SpannerBuiltInMetricsIntegrationTests
{
private static readonly TimeSpan s_pollingTimeout = TimeSpan.FromMinutes(3);
private static readonly TimeSpan s_pollingInterval = TimeSpan.FromSeconds(10);

private static readonly string[] s_expectedMetrics =
[
"spanner.googleapis.com/client/operation_count",
"spanner.googleapis.com/client/operation_latencies",
"spanner.googleapis.com/client/attempt_count",
"spanner.googleapis.com/client/attempt_latencies",
"spanner.googleapis.com/client/gfe_latencies",
"spanner.googleapis.com/client/gfe_connectivity_error_count"
];

[Fact]
public async Task BuiltInMetrics_ExportsAllSpannerTelemetryToMonitoring()
{
// Arrange the metrics to be emittted along with their labels
string projectId = TestEnvironment.GetTestProjectId();
var credential = await GoogleCredential.GetApplicationDefaultAsync();
string testClientName = $"spanner.net.test.{Guid.NewGuid().ToString().Substring(0, 5)}";
string testUid = Guid.NewGuid().ToString();

using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("spanner-built-in-metrics-integration-test", "test")
.AddAttributes(
[
new KeyValuePair<string, object>("gcp.resource_type", "spanner_instance_client"),
new KeyValuePair<string, object>("gcp.project_id", projectId),
new KeyValuePair<string, object>("project_id", projectId),
new KeyValuePair<string, object>("location", "us-central1"),
new KeyValuePair<string, object>("instance_id", "test-spanner-instance"),
new KeyValuePair<string, object>("instance_config", "unknown"),
new KeyValuePair<string, object>("client_hash", "1234abcd"),
]))
.AddGoogleCloudBuiltInMetrics(credential)
.Build();

var meter = new Meter(SpannerMetricsConfiguration.MeterName);

var operationCounter = meter.CreateCounter<long>("operation_count");
var operationLatency = meter.CreateHistogram<double>("operation_latencies");
var attemptCounter = meter.CreateCounter<long>("attempt_count");
var attemptLatency = meter.CreateHistogram<double>("attempt_latencies");
var gfeLatency = meter.CreateHistogram<double>("gfe_latencies");
var gfeErrorCounter = meter.CreateCounter<long>("gfe_connectivity_error_count");

var labels = new KeyValuePair<string, object>[]
{
new("database", "test-database"),
new("method", "ExecuteSql"),
new("status", "OK"),
new("client_name", testClientName),
new("client_uid", testUid),
new("directpath_enabled", false),
new("directpath_used", false)
};

// Emit all metrics together so they export in a single batch,
// running tests individualy would be too slow (1-3 minutes each)
for (int i = 0; i < 5; i++)
{
operationCounter.Add(1, labels);
operationLatency.Record(125.0, labels);
attemptCounter.Add(1, labels);
attemptLatency.Record(80.0, labels);
gfeLatency.Record(15.0, labels);
gfeErrorCounter.Add(1, labels);
await Task.Delay(200);
}

meterProvider.ForceFlush();

var client = new MetricServiceClientBuilder
{
Credential = credential
}.Build();

// Ensure all metrics were registered to the spanner built in metrics resource
var verificationTasks = s_expectedMetrics.Select(metricType =>
WaitForMetricAsync(client, projectId, metricType, testClientName));
await Task.WhenAll(verificationTasks);
}

private static async Task WaitForMetricAsync(
MetricServiceClient client,
string projectId,
string metricType,
string expectedClientName)
{
var startTime = DateTime.UtcNow;
var deadline = startTime + s_pollingTimeout;

var request = new ListTimeSeriesRequest
{
Name = $"projects/{projectId}",
Filter = $"metric.type = \"{metricType}\" AND metric.labels.client_name = \"{expectedClientName}\"",
Interval = new TimeInterval
{
StartTime = Timestamp.FromDateTime(startTime.AddMinutes(-5)),
EndTime = Timestamp.FromDateTime(startTime.AddMinutes(5))
}
};

while (DateTime.UtcNow < deadline)
{
try
{
var response = await client.ListTimeSeriesAsync(request).ReadPageAsync(100);
if (response.Any(series => series.Points.Count != 0))
{
return;
}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound || ex.StatusCode == StatusCode.InvalidArgument)
{
// Metric not yet ingested or indexed
}

await Task.Delay(s_pollingInterval);
}

Assert.Fail($"Timed out waiting {s_pollingTimeout.TotalMinutes} minutes for metric '{metricType}' with client_name='{expectedClientName}' to appear in Cloud Monitoring.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net462</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);1701;1702;1705;xUnit2004;xUnit2013</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Api.Gax.Testing" />
<ProjectReference Include="..\Google.Cloud.BuiltInMetrics\Google.Cloud.BuiltInMetrics.csproj" />
<ProjectReference Include="..\..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj" />
<PackageReference Include="Google.Cloud.Monitoring.V3" VersionOverride="[3.10.0, 4.0.0)" />
<ProjectReference Include="..\..\Google.Cloud.Spanner.Common.V1\Google.Cloud.Spanner.Common.V1\Google.Cloud.Spanner.Common.V1.csproj" />
<ProjectReference Include="..\..\Google.Cloud.Spanner.V1\Google.Cloud.Spanner.V1\Google.Cloud.Spanner.V1.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="Google.Cloud.BuiltInMetrics.Tests.csproj.google" Condition="Exists('Google.Cloud.BuiltInMetrics.Tests.csproj.google')" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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.

using OpenTelemetry;
using OpenTelemetry.Metrics;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using Xunit;

namespace Google.Cloud.BuiltInMetrics.Tests;

public class SpannerMetricsConfigurationTests
{
public enum MetricInstrumentType
{
Counter,
Histogram
}

[Theory]
[InlineData("operation_count", MetricInstrumentType.Counter)]
[InlineData("attempt_count", MetricInstrumentType.Counter)]
[InlineData("gfe_connectivity_error_count", MetricInstrumentType.Counter)]
[InlineData("afe_connectivity_error_count", MetricInstrumentType.Counter)]
[InlineData("operation_latencies", MetricInstrumentType.Histogram)]
[InlineData("attempt_latencies", MetricInstrumentType.Histogram)]
[InlineData("gfe_latencies", MetricInstrumentType.Histogram)]
[InlineData("afe_latencies", MetricInstrumentType.Histogram)]
public void Configure_MapsInstrumentsToInternalPrefix(
string instrumentName,
MetricInstrumentType instrumentType)
{
var exportedMetrics = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddTestExporter(exportedMetrics)
.ConfigureSpannerMetrics()
.Build();

var meter = new Meter(SpannerMetricsConfiguration.MeterName);
if (instrumentType == MetricInstrumentType.Counter)
{
meter.CreateCounter<long>(instrumentName).Add(1);
}
else
{
meter.CreateHistogram<double>(instrumentName).Record(1);
}

meterProvider.ForceFlush();

var metric = Assert.Single(exportedMetrics);
// The metrics should have spanner.googleapis.com/internal/client/ as a prefix
Assert.Equal(SpannerMetricsConfiguration.InternalMetricPrefix + instrumentName, metric.Name);
}

[Fact]
public void Configure_UnregisteredMeter_IsIgnored()
{
var exportedMetrics = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddTestExporter(exportedMetrics)
.ConfigureSpannerMetrics()
.Build();

var unregisteredMeter = new Meter("Some.Other.Unregistered.Meter");
unregisteredMeter.CreateCounter<long>("operation_count").Add(10);

meterProvider.ForceFlush();

Assert.Empty(exportedMetrics);
}

[Fact]
public void Configure_AnotherMeter_IsNotTransformedBySpannerViews()
{
var exportedMetrics = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("CustomApp.Meter")
.AddTestExporter(exportedMetrics)
.ConfigureSpannerMetrics()
.Build();

var customMeter = new Meter("CustomApp.Meter");
customMeter.CreateCounter<long>("operation_count").Add(10);

meterProvider.ForceFlush();

var metric = Assert.Single(exportedMetrics);
Assert.Equal("operation_count", metric.Name);
}
}
Loading
Loading