Skip to content

Commit fb6c5f4

Browse files
authored
fix(v2): lazy initialization and getters for ClientMetrics (#485)
* refactor: use getter-style methods for ClientMetrics defaults * refactor: use sync.OnceValue for strict encapsulation of ClientMetrics lazy initialization and add logger
1 parent 336f424 commit fb6c5f4

File tree

2 files changed

+191
-65
lines changed

2 files changed

+191
-65
lines changed

v2/telemetry.go

Lines changed: 100 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ package gax
3131

3232
import (
3333
"context"
34+
"log/slog"
35+
"sync"
3436

3537
"go.opentelemetry.io/otel"
3638
"go.opentelemetry.io/otel/attribute"
@@ -136,112 +138,158 @@ var defaultHistogramBoundaries = []float64{
136138
// for a specific generated Google Cloud client library.
137139
// There should be exactly one ClientMetrics instance instantiated per generated client.
138140
type ClientMetrics struct {
141+
get func() clientMetricsData
142+
}
143+
144+
type clientMetricsData struct {
139145
duration metric.Float64Histogram
140146
attr []attribute.KeyValue
141147
}
142148

143-
type clientMetricsOptions struct {
149+
type telemetryOptions struct {
144150
provider metric.MeterProvider
145151
attributes map[string]string
146152
explicitBucketBoundaries []float64
153+
logger *slog.Logger
147154
}
148155

149-
// ClientMetricsOption is an option to configure a ClientMetrics instance.
150-
// ClientMetricsOption works by modifying relevant fields of clientMetricsOptions.
151-
type ClientMetricsOption interface {
156+
// TelemetryOption is an option to configure a ClientMetrics instance.
157+
// TelemetryOption works by modifying relevant fields of telemetryOptions.
158+
type TelemetryOption interface {
152159
// Resolve applies the option by modifying opts.
153-
Resolve(opts *clientMetricsOptions)
160+
Resolve(opts *telemetryOptions)
154161
}
155162

156163
type providerOpt struct {
157164
p metric.MeterProvider
158165
}
159166

160-
func (p providerOpt) Resolve(opts *clientMetricsOptions) {
167+
func (p providerOpt) Resolve(opts *telemetryOptions) {
161168
opts.provider = p.p
162169
}
163170

164171
// WithMeterProvider specifies the metric.MeterProvider to use for instruments.
165-
func WithMeterProvider(p metric.MeterProvider) ClientMetricsOption {
172+
func WithMeterProvider(p metric.MeterProvider) TelemetryOption {
166173
return &providerOpt{p: p}
167174
}
168175

169176
type attrOpt struct {
170177
attrs map[string]string
171178
}
172179

173-
func (a attrOpt) Resolve(opts *clientMetricsOptions) {
180+
func (a attrOpt) Resolve(opts *telemetryOptions) {
174181
opts.attributes = a.attrs
175182
}
176183

177-
// WithClientMetricsAttributes specifies the static attributes attachments.
178-
func WithClientMetricsAttributes(attr map[string]string) ClientMetricsOption {
184+
// WithTelemetryAttributes specifies the static attributes attachments.
185+
func WithTelemetryAttributes(attr map[string]string) TelemetryOption {
179186
return &attrOpt{attrs: attr}
180187
}
181188

182189
type boundariesOpt struct {
183190
boundaries []float64
184191
}
185192

186-
func (b boundariesOpt) Resolve(opts *clientMetricsOptions) {
193+
func (b boundariesOpt) Resolve(opts *telemetryOptions) {
187194
opts.explicitBucketBoundaries = b.boundaries
188195
}
189196

190197
// WithExplicitBucketBoundaries overrides the default histogram bucket boundaries.
191-
func WithExplicitBucketBoundaries(boundaries []float64) ClientMetricsOption {
198+
func WithExplicitBucketBoundaries(boundaries []float64) TelemetryOption {
192199
return &boundariesOpt{boundaries: boundaries}
193200
}
194201

195-
// NewClientMetrics initializes and returns a new ClientMetrics instance.
196-
// It is intended to be called once per generated client during initialization.
197-
func NewClientMetrics(opts ...ClientMetricsOption) *ClientMetrics {
198-
var config clientMetricsOptions
199-
for _, opt := range opts {
200-
opt.Resolve(&config)
201-
}
202+
type loggerOpt struct {
203+
l *slog.Logger
204+
}
202205

203-
provider := config.provider
204-
if provider == nil {
205-
provider = otel.GetMeterProvider()
206-
}
206+
func (l loggerOpt) Resolve(opts *telemetryOptions) {
207+
opts.logger = l.l
208+
}
207209

208-
var meterAttrs []attribute.KeyValue
209-
if val, ok := config.attributes[ClientService]; ok {
210-
meterAttrs = append(meterAttrs, attribute.KeyValue{Key: attribute.Key(keyGCPClientService), Value: attribute.StringValue(val)})
211-
}
210+
// WithTelemetryLogger specifies a logger to record internal telemetry errors.
211+
func WithTelemetryLogger(l *slog.Logger) TelemetryOption {
212+
return &loggerOpt{l: l}
213+
}
212214

213-
meterOpts := []metric.MeterOption{
214-
metric.WithInstrumentationVersion(config.attributes[ClientVersion]),
215-
metric.WithSchemaURL(schemaURL),
216-
}
217-
if len(meterAttrs) > 0 {
218-
meterOpts = append(meterOpts, metric.WithInstrumentationAttributes(meterAttrs...))
215+
func (config *telemetryOptions) meterProvider() metric.MeterProvider {
216+
if config.provider != nil {
217+
return config.provider
219218
}
219+
return otel.GetMeterProvider()
220+
}
220221

221-
meter := provider.Meter(config.attributes[ClientArtifact], meterOpts...)
222-
223-
boundaries := config.explicitBucketBoundaries
224-
if len(boundaries) == 0 {
225-
boundaries = defaultHistogramBoundaries
222+
func (config *telemetryOptions) bucketBoundaries() []float64 {
223+
if len(config.explicitBucketBoundaries) > 0 {
224+
return config.explicitBucketBoundaries
226225
}
226+
return defaultHistogramBoundaries
227+
}
227228

228-
duration, _ := meter.Float64Histogram(
229-
metricName,
230-
metric.WithDescription(metricDescription),
231-
metric.WithUnit("s"),
232-
metric.WithExplicitBucketBoundaries(boundaries...),
233-
)
229+
// NewClientMetrics initializes and returns a new ClientMetrics instance.
230+
// It is intended to be called once per generated client during initialization.
231+
func NewClientMetrics(opts ...TelemetryOption) *ClientMetrics {
232+
var config telemetryOptions
233+
for _, opt := range opts {
234+
opt.Resolve(&config)
235+
}
234236

235-
var attr []attribute.KeyValue
236-
if val, ok := config.attributes[URLDomain]; ok {
237-
attr = append(attr, attribute.KeyValue{Key: attribute.Key(keyURLDomain), Value: attribute.StringValue(val)})
237+
return &ClientMetrics{
238+
get: sync.OnceValue(func() clientMetricsData {
239+
provider := config.meterProvider()
240+
241+
var meterAttrs []attribute.KeyValue
242+
if val, ok := config.attributes[ClientService]; ok {
243+
meterAttrs = append(meterAttrs, attribute.KeyValue{Key: attribute.Key(keyGCPClientService), Value: attribute.StringValue(val)})
244+
}
245+
246+
meterOpts := []metric.MeterOption{
247+
metric.WithInstrumentationVersion(config.attributes[ClientVersion]),
248+
metric.WithSchemaURL(schemaURL),
249+
}
250+
if len(meterAttrs) > 0 {
251+
meterOpts = append(meterOpts, metric.WithInstrumentationAttributes(meterAttrs...))
252+
}
253+
254+
meter := provider.Meter(config.attributes[ClientArtifact], meterOpts...)
255+
256+
boundaries := config.bucketBoundaries()
257+
258+
duration, err := meter.Float64Histogram(
259+
metricName,
260+
metric.WithDescription(metricDescription),
261+
metric.WithUnit("s"),
262+
metric.WithExplicitBucketBoundaries(boundaries...),
263+
)
264+
if err != nil && config.logger != nil {
265+
config.logger.Warn("failed to initialize OTel duration histogram", "error", err)
266+
}
267+
268+
var attr []attribute.KeyValue
269+
if val, ok := config.attributes[URLDomain]; ok {
270+
attr = append(attr, attribute.KeyValue{Key: attribute.Key(keyURLDomain), Value: attribute.StringValue(val)})
271+
}
272+
if val, ok := config.attributes[RPCSystem]; ok {
273+
attr = append(attr, attribute.KeyValue{Key: attribute.Key(keyRPCSystemName), Value: attribute.StringValue(val)})
274+
}
275+
return clientMetricsData{
276+
duration: duration,
277+
attr: attr,
278+
}
279+
}),
238280
}
239-
if val, ok := config.attributes[RPCSystem]; ok {
240-
attr = append(attr, attribute.KeyValue{Key: attribute.Key(keyRPCSystemName), Value: attribute.StringValue(val)})
281+
}
282+
283+
func (cm *ClientMetrics) durationHistogram() metric.Float64Histogram {
284+
if cm == nil || cm.get == nil {
285+
return nil
241286
}
287+
return cm.get().duration
288+
}
242289

243-
return &ClientMetrics{
244-
duration: duration,
245-
attr: attr,
290+
func (cm *ClientMetrics) attributes() []attribute.KeyValue {
291+
if cm == nil || cm.get == nil {
292+
return nil
246293
}
294+
return cm.get().attr
247295
}

0 commit comments

Comments
 (0)