Skip to content

Commit e9d2efd

Browse files
authored
feat(bigtable): expose otel native grpc metrics in Bigtable (#12827)
Scrapes the selected Otel metrics and exports the metrics to Cloud Bigtable Client schema. This change instruments the Bigtable Go client to export a standard set of gRPC metrics to Google Cloud Monitoring using OpenTelemetry. The following metrics are now exported under the bigtable.googleapis.com/internal/client/ prefix: - grpc.client.attempt.duration - grpc.lb.rls.default_target_picks - grpc.lb.rls.target_picks - grpc.lb.rls.failed_picks - grpc.xds_client.server_failure - grpc.xds_client.resource_updates_invalid These metrics are exported to a new Cloud Monitoring monitored resource type: bigtable_client. This implementation follows the existing pattern within the metrics infrastructure where failures during metrics initialization do not prevent the successful creation of the Bigtable client, prioritizing application availability over metrics collection. Unit tests have been added to validate the new functionality.
1 parent a9d36c0 commit e9d2efd

6 files changed

Lines changed: 415 additions & 5 deletions

File tree

bigtable/bigtable.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ func NewClientWithConfig(ctx context.Context, project, instance string, config C
124124
if err != nil {
125125
return nil, err
126126
}
127+
// for otel metrics
128+
if metricsTracerFactory.enabled {
129+
if len(metricsTracerFactory.clientOpts) > 0 {
130+
o = append(o, metricsTracerFactory.clientOpts...)
131+
}
132+
}
133+
127134
// Add gRPC client interceptors to supply Google client information. No external interceptors are passed.
128135
o = append(o, btopt.ClientInterceptorOptions(nil, nil)...)
129136
o = append(o, option.WithGRPCDialOption(grpc.WithStatsHandler(sharedLatencyStatsHandler)))

bigtable/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ require (
77
cloud.google.com/go/iam v1.5.2
88
cloud.google.com/go/longrunning v0.6.7
99
cloud.google.com/go/monitoring v1.24.2
10+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
1011
github.com/google/btree v1.1.3
1112
github.com/google/go-cmp v0.7.0
1213
github.com/google/uuid v1.6.0
1314
github.com/googleapis/cloud-bigtable-clients-test v0.0.4
1415
github.com/googleapis/gax-go/v2 v2.15.0
16+
go.opentelemetry.io/contrib/detectors/gcp v1.36.0
1517
go.opentelemetry.io/otel v1.36.0
1618
go.opentelemetry.io/otel/metric v1.36.0
1719
go.opentelemetry.io/otel/sdk v1.36.0
@@ -30,6 +32,8 @@ require (
3032
cloud.google.com/go/auth v0.16.4 // indirect
3133
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
3234
cloud.google.com/go/compute/metadata v0.8.0 // indirect
35+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
36+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
3337
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3438
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
3539
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect

bigtable/go.sum

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,28 @@ cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcao
1010
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
1111
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
1212
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
13+
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
14+
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
1315
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
1416
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
1517
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
1618
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
19+
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
20+
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
21+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
22+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
23+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
24+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
25+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
26+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
27+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
28+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
1729
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1830
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
1931
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
2032
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
21-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
34+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2335
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
2436
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
2537
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
@@ -55,8 +67,8 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81
5567
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
5668
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
5769
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
58-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
59-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
70+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
71+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6072
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
6173
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
6274
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -65,6 +77,8 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
6577
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
6678
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
6779
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
80+
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
81+
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
6882
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
6983
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
7084
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=

bigtable/metrics.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ type metricInfo struct {
186186
type builtinMetricsTracerFactory struct {
187187
enabled bool
188188

189+
clientOpts []option.ClientOption
190+
189191
// To be called on client close
190192
shutdown func()
191193

@@ -241,7 +243,30 @@ func newBuiltinMetricsTracerFactory(ctx context.Context, project, instance, appP
241243
return disabledMetricsTracerFactory, nil
242244
}
243245
meterProvider := sdkmetric.NewMeterProvider(mpOptions...)
244-
tracerFactory.shutdown = func() { meterProvider.Shutdown(ctx) }
246+
// Enable Otel metrics collection
247+
otelContext, err := newOtelMetricsContext(ctx, metricsConfig{
248+
project: project,
249+
instance: instance,
250+
appProfile: appProfile,
251+
clientName: clientName,
252+
clientUID: clientUID,
253+
interval: defaultSamplePeriod,
254+
customExporter: nil,
255+
manualReader: nil,
256+
disableExporter: false,
257+
resourceOpts: nil,
258+
})
259+
260+
// the error from newOtelMetricsContext is silently ignored since metrics are not critical to client creation.
261+
if err == nil {
262+
tracerFactory.clientOpts = otelContext.clientOpts
263+
}
264+
tracerFactory.shutdown = func() {
265+
if otelContext != nil {
266+
otelContext.close()
267+
}
268+
meterProvider.Shutdown(ctx)
269+
}
245270

246271
// Create meter and instruments
247272
meter := meterProvider.Meter(builtInMetricsMeterName, metric.WithInstrumentationVersion(internal.Version))

bigtable/otel_metrics.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package bigtable
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
"time"
24+
25+
mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
26+
"go.opentelemetry.io/contrib/detectors/gcp"
27+
"go.opentelemetry.io/otel/attribute"
28+
"go.opentelemetry.io/otel/sdk/metric"
29+
"go.opentelemetry.io/otel/sdk/metric/metricdata"
30+
"go.opentelemetry.io/otel/sdk/resource"
31+
"google.golang.org/api/option"
32+
"google.golang.org/grpc"
33+
"google.golang.org/grpc/experimental/stats"
34+
"google.golang.org/grpc/stats/opentelemetry"
35+
)
36+
37+
const (
38+
bigtableClientMonitoredResourceName = "bigtable_client"
39+
bigtableClientMetricPrefix = "bigtable.googleapis.com/internal/client/"
40+
unKnownAtttr = "unknown"
41+
)
42+
43+
var latenciesBoundaries = []float64{0.0, 0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.008, 0.01, 0.013, 0.016, 0.02, 0.025, 0.03, 0.04, 0.05, 0.065, 0.08, 0.1, 0.13, 0.16, 0.2, 0.25, 0.3, 0.4, 0.5, 0.65, 0.8, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0} // max is 53.3 minutes
44+
45+
// bigtable_client monitored resource labels
46+
type bigtableClientMonitoredResource struct {
47+
project string // project
48+
instance string // instance
49+
appProfile string // app_profile
50+
clientProject string // client_project
51+
cloudPlatform string // cloud_platform
52+
region string // client_region
53+
hostID string // host_id
54+
hostName string // host_name
55+
clientName string // client_name
56+
clientUID string // uuid
57+
resource *resource.Resource
58+
}
59+
60+
func (bmr *bigtableClientMonitoredResource) exporter() (metric.Exporter, error) {
61+
exporter, err := mexporter.New(
62+
mexporter.WithProjectID(bmr.clientProject),
63+
mexporter.WithMetricDescriptorTypeFormatter(metricFormatter),
64+
mexporter.WithCreateServiceTimeSeries(),
65+
mexporter.WithMonitoredResourceDescription(bigtableClientMonitoredResourceName, []string{"project_id", "instance", "app_profile", "client_project", "cloud_platform", "host_id", "host_name", "client_name", "uuid", "region"}),
66+
)
67+
if err != nil {
68+
return nil, fmt.Errorf("bigtable: creating metrics exporter: %w", err)
69+
}
70+
return exporter, nil
71+
}
72+
73+
func metricFormatter(m metricdata.Metrics) string {
74+
// converts grpc.lb.rls.target_picks to `bigtable.googleapis.com/internal/client/grpc/lb/rls/target_picks`
75+
return bigtableClientMetricPrefix + strings.ReplaceAll(string(m.Name), ".", "/")
76+
}
77+
78+
func getAttribute(s *attribute.Set, key attribute.Key, defaultValue string) string {
79+
if val, ok := s.Value(key); ok {
80+
return val.AsString()
81+
}
82+
return defaultValue
83+
}
84+
85+
func newBigtableClientMonitoredResource(ctx context.Context, project, appProfile, instance, clientName, clientUID string, opts ...resource.Option) (*bigtableClientMonitoredResource, error) {
86+
detectedAttrs, err := resource.New(ctx, opts...)
87+
if err != nil {
88+
return nil, err
89+
}
90+
smr := &bigtableClientMonitoredResource{
91+
project: project,
92+
instance: instance,
93+
appProfile: appProfile,
94+
clientName: clientName,
95+
clientUID: clientUID,
96+
}
97+
s := detectedAttrs.Set()
98+
// Attempt to use resource detector project id if project id wasn't
99+
// identified using ADC as a last resort. Otherwise metrics cannot be started.
100+
101+
smr.clientProject = getAttribute(s, "cloud.account.id", unKnownAtttr)
102+
smr.cloudPlatform = getAttribute(s, "cloud.platform", unKnownAtttr)
103+
smr.hostName = getAttribute(s, "host.name", unKnownAtttr)
104+
smr.hostID = getAttribute(s, "host.id", unKnownAtttr)
105+
if smr.hostID == "unknown" {
106+
// cloud run / cloud functions have faas.id instead of host.id
107+
108+
smr.hostID = getAttribute(s, "faas.id", unKnownAtttr)
109+
}
110+
smr.region = getAttribute(s, "cloud.region", "global")
111+
smr.resource, err = resource.New(ctx, resource.WithAttributes([]attribute.KeyValue{
112+
{Key: "gcp.resource_type", Value: attribute.StringValue(bigtableClientMonitoredResourceName)},
113+
{Key: "project_id", Value: attribute.StringValue(project)},
114+
{Key: "app_profile", Value: attribute.StringValue(smr.appProfile)},
115+
{Key: "region", Value: attribute.StringValue(smr.region)},
116+
{Key: "instance", Value: attribute.StringValue(smr.instance)},
117+
{Key: "cloud_platform", Value: attribute.StringValue(smr.cloudPlatform)},
118+
{Key: "host_id", Value: attribute.StringValue(smr.hostID)},
119+
{Key: "host_name", Value: attribute.StringValue(smr.hostName)},
120+
{Key: "client_name", Value: attribute.StringValue(smr.clientName)},
121+
{Key: "uuid", Value: attribute.StringValue(smr.clientUID)},
122+
{Key: "client_project", Value: attribute.StringValue(smr.clientProject)},
123+
}...))
124+
if err != nil {
125+
return nil, err
126+
}
127+
return smr, nil
128+
}
129+
130+
type metricsContext struct {
131+
// client options passed to gRPC channels
132+
clientOpts []option.ClientOption
133+
// instance of metric reader used by gRPC client-side metrics
134+
provider *metric.MeterProvider
135+
// clean func to call when closing gRPC client
136+
close func()
137+
}
138+
139+
type metricsConfig struct {
140+
project string // project_id
141+
instance string // instance
142+
appProfile string // app_profile
143+
clientName string // client_name
144+
clientUID string // uuid
145+
interval time.Duration
146+
customExporter *metric.Exporter
147+
manualReader *metric.ManualReader // used by tests
148+
disableExporter bool // used by tests disables exports
149+
resourceOpts []resource.Option // used by tests
150+
}
151+
152+
func newOtelMetricsContext(ctx context.Context, cfg metricsConfig) (*metricsContext, error) {
153+
var exporter metric.Exporter
154+
meterOpts := []metric.Option{}
155+
if cfg.customExporter == nil {
156+
var ropts []resource.Option
157+
if cfg.resourceOpts != nil {
158+
ropts = cfg.resourceOpts
159+
} else {
160+
ropts = []resource.Option{resource.WithDetectors(gcp.NewDetector())}
161+
}
162+
smr, err := newBigtableClientMonitoredResource(ctx, cfg.project, cfg.appProfile, cfg.instance, cfg.clientName, cfg.clientUID, ropts...)
163+
if err != nil {
164+
return nil, err
165+
}
166+
exporter, err = smr.exporter()
167+
if err != nil {
168+
return nil, err
169+
}
170+
meterOpts = append(meterOpts, metric.WithResource(smr.resource))
171+
} else {
172+
exporter = *cfg.customExporter
173+
}
174+
interval := time.Minute
175+
if cfg.interval > 0 {
176+
interval = cfg.interval
177+
}
178+
meterOpts = append(meterOpts,
179+
// customer histogram boundaries
180+
metric.WithView(
181+
createHistogramView("grpc.client.attempt.duration", latenciesBoundaries),
182+
))
183+
if cfg.manualReader != nil {
184+
meterOpts = append(meterOpts, metric.WithReader(cfg.manualReader))
185+
}
186+
if !cfg.disableExporter {
187+
meterOpts = append(meterOpts, metric.WithReader(
188+
metric.NewPeriodicReader(&exporterLogSuppressor{Exporter: exporter}, metric.WithInterval(interval))))
189+
}
190+
provider := metric.NewMeterProvider(meterOpts...)
191+
mo := opentelemetry.MetricsOptions{
192+
MeterProvider: provider,
193+
Metrics: stats.NewMetrics(
194+
"grpc.client.attempt.duration",
195+
"grpc.lb.rls.default_target_picks",
196+
"grpc.lb.rls.target_picks",
197+
"grpc.lb.rls.failed_picks",
198+
"grpc.xds_client.server_failure",
199+
"grpc.xds_client.resource_updates_invalid",
200+
),
201+
OptionalLabels: []string{"grpc.lb.locality"},
202+
}
203+
opts := []option.ClientOption{
204+
option.WithGRPCDialOption(
205+
opentelemetry.DialOption(opentelemetry.Options{MetricsOptions: mo})),
206+
option.WithGRPCDialOption(
207+
grpc.WithDefaultCallOptions(grpc.StaticMethodCallOption{})),
208+
}
209+
return &metricsContext{
210+
clientOpts: opts,
211+
provider: provider,
212+
close: func() {
213+
provider.Shutdown(ctx)
214+
},
215+
}, nil
216+
}
217+
218+
// Silences permission errors after initial error is emitted to prevent
219+
// chatty logs.
220+
type exporterLogSuppressor struct {
221+
metric.Exporter
222+
emittedFailure bool
223+
}
224+
225+
// Implements OTel SDK metric.Exporter interface to prevent noisy logs from
226+
// lack of credentials after initial failure.
227+
func (e *exporterLogSuppressor) Export(ctx context.Context, rm *metricdata.ResourceMetrics) error {
228+
if err := e.Exporter.Export(ctx, rm); err != nil && !e.emittedFailure {
229+
if strings.Contains(err.Error(), "PermissionDenied") {
230+
e.emittedFailure = true
231+
return fmt.Errorf("gRPC metrics failed due permission issue: %w", err)
232+
}
233+
return err
234+
}
235+
return nil
236+
}
237+
238+
func createHistogramView(name string, boundaries []float64) metric.View {
239+
return metric.NewView(metric.Instrument{
240+
Name: name,
241+
Kind: metric.InstrumentKindHistogram,
242+
}, metric.Stream{
243+
Name: name,
244+
Aggregation: metric.AggregationExplicitBucketHistogram{Boundaries: boundaries},
245+
})
246+
}

0 commit comments

Comments
 (0)