Skip to content

Commit 845766f

Browse files
committed
[Yarpc-Go] Implement Metrics Tag Decorator
Update Comment address address comment resolve lint error update copyright changing interface location gRPC reflection server (#2371) * gRPC reflection server * license header * add README.md for gRPC reflection * updated .codecov.yml [Yarpc-Go] Implement Metrics Tag Decorator Update Comment address address comment resolve lint error update copyright changing interface location gRPC reflection server (#2371) * gRPC reflection server * license header * add README.md for gRPC reflection * updated .codecov.yml nit Revert "gRPC reflection server (#2371)" This reverts commit 056f218. nit limiting logging per edge limiting logging per edge nits decoratortags as pointer adding sampledLogger + modifying interface remove ctx remove ctx adding decorator call options using context
1 parent f81211d commit 845766f

File tree

7 files changed

+229
-17
lines changed

7 files changed

+229
-17
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package metricstagdecorator
22+
23+
import (
24+
"context"
25+
)
26+
27+
// MetricsTagDecorator is used for adding custom tags to YARPC metrics.
28+
type MetricsTagDecorator interface {
29+
ProvideTags(ctx context.Context) map[string]string
30+
}

config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/uber-go/tally"
2929
"go.uber.org/net/metrics"
3030
"go.uber.org/net/metrics/tallypush"
31+
"go.uber.org/yarpc/api/metrics/metricstagdecorator"
3132
"go.uber.org/yarpc/api/middleware"
3233
"go.uber.org/yarpc/internal/observability"
3334
"go.uber.org/zap"
@@ -157,6 +158,8 @@ type MetricsConfig struct {
157158
// TagsBlocklist enlists tags' keys that should be suppressed from all the metrics
158159
// emitted from w/in YARPC middleware.
159160
TagsBlocklist []string
161+
// MetricsTagsDecorators populates yarpc metrics scope with custom tags.
162+
MetricsTagsDecorators []metricstagdecorator.MetricsTagDecorator
160163
}
161164

162165
func (c MetricsConfig) scope(name string, logger *zap.Logger) (*metrics.Scope, context.CancelFunc) {

dispatcher.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ func addObservingMiddleware(cfg Config, meter *metrics.Scope, logger *zap.Logger
103103
}
104104

105105
observer := observability.NewMiddleware(observability.Config{
106-
Logger: logger,
107-
Scope: meter,
108-
ContextExtractor: extractor,
109-
MetricTagsBlocklist: cfg.Metrics.TagsBlocklist,
106+
Logger: logger,
107+
Scope: meter,
108+
ContextExtractor: extractor,
109+
MetricTagsBlocklist: cfg.Metrics.TagsBlocklist,
110+
MetricsTagsDecorators: cfg.Metrics.MetricsTagsDecorators,
110111
Levels: observability.LevelsConfig{
111112
Default: observability.DirectionalLevelsConfig{
112113
Success: cfg.Logging.Levels.Success,

internal/observability/graph.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@ import (
2727

2828
"go.uber.org/net/metrics"
2929
"go.uber.org/net/metrics/bucket"
30+
"go.uber.org/yarpc/api/metrics/metricstagdecorator"
3031
"go.uber.org/yarpc/api/transport"
3132
"go.uber.org/yarpc/internal/digester"
33+
"go.uber.org/yarpc/internal/sampledlogger"
3234
"go.uber.org/zap"
3335
"go.uber.org/zap/zapcore"
3436
)
3537

3638
var (
3739
_timeNow = time.Now // for tests
3840
_defaultGraphSize = 128
41+
_logInterval = time.Duration(5 * time.Minute)
42+
3943
// Latency buckets for histograms. At some point, we may want to make these
4044
// configurable.
4145
_bucketsMs = bucket.NewRPCLatency()
@@ -72,9 +76,10 @@ type graph struct {
7276
logger *zap.Logger
7377
extract ContextExtractor
7478
ignoreMetricsTag *metricsTagIgnore
75-
76-
edgesMu sync.RWMutex
77-
edges map[string]*edge
79+
decoratorTags []metricstagdecorator.MetricsTagDecorator
80+
sampledLogger *sampledlogger.SampledLogger
81+
edgesMu sync.RWMutex
82+
edges map[string]*edge
7883

7984
inboundLevels, outboundLevels levels
8085
}
@@ -164,13 +169,15 @@ func (m *metricsTagIgnore) tags(req *transport.Request, direction string, rpcTyp
164169
return tags
165170
}
166171

167-
func newGraph(meter *metrics.Scope, logger *zap.Logger, extract ContextExtractor, metricTagsIgnore []string) graph {
172+
func newGraph(meter *metrics.Scope, logger *zap.Logger, extract ContextExtractor, metricTagsIgnore []string, metricsTagsDecorators []metricstagdecorator.MetricsTagDecorator) graph {
168173
return graph{
169174
edges: make(map[string]*edge, _defaultGraphSize),
170175
meter: meter,
171176
logger: logger,
172177
extract: extract,
173178
ignoreMetricsTag: newMetricsTagIgnore(metricTagsIgnore),
179+
decoratorTags: metricsTagsDecorators,
180+
sampledLogger: sampledlogger.NewSampledLogger(_logInterval, logger),
174181
inboundLevels: levels{
175182
success: zapcore.DebugLevel,
176183
failure: zapcore.ErrorLevel,
@@ -220,7 +227,7 @@ func (g *graph) begin(ctx context.Context, rpcType transport.Type, direction dir
220227
if !g.ignoreMetricsTag.rpcType {
221228
d.Add(rpcType.String())
222229
}
223-
e := g.getOrCreateEdge(d.Digest(), req, string(direction), rpcType)
230+
e := g.getOrCreateEdge(ctx, d.Digest(), req, string(direction), rpcType)
224231
d.Free()
225232

226233
levels := &g.inboundLevels
@@ -240,11 +247,11 @@ func (g *graph) begin(ctx context.Context, rpcType transport.Type, direction dir
240247
}
241248
}
242249

243-
func (g *graph) getOrCreateEdge(key []byte, req *transport.Request, direction string, rpcType transport.Type) *edge {
250+
func (g *graph) getOrCreateEdge(ctx context.Context, key []byte, req *transport.Request, direction string, rpcType transport.Type) *edge {
244251
if e := g.getEdge(key); e != nil {
245252
return e
246253
}
247-
return g.createEdge(key, req, direction, rpcType)
254+
return g.createEdge(ctx, key, req, direction, rpcType)
248255
}
249256

250257
func (g *graph) getEdge(key []byte) *edge {
@@ -254,7 +261,7 @@ func (g *graph) getEdge(key []byte) *edge {
254261
return e
255262
}
256263

257-
func (g *graph) createEdge(key []byte, req *transport.Request, direction string, rpcType transport.Type) *edge {
264+
func (g *graph) createEdge(ctx context.Context, key []byte, req *transport.Request, direction string, rpcType transport.Type) *edge {
258265
g.edgesMu.Lock()
259266
// Since we'll rarely hit this code path, the overhead of defer is acceptable.
260267
defer g.edgesMu.Unlock()
@@ -264,7 +271,7 @@ func (g *graph) createEdge(key []byte, req *transport.Request, direction string,
264271
return e
265272
}
266273

267-
e := newEdge(g.logger, g.meter, g.ignoreMetricsTag, req, direction, rpcType)
274+
e := newEdge(ctx, g.logger, g.sampledLogger, g.meter, g.ignoreMetricsTag, g.decoratorTags, req, direction, rpcType)
268275
g.edges[string(key)] = e
269276
return e
270277
}
@@ -309,9 +316,17 @@ type streamEdge struct {
309316

310317
// newEdge constructs a new edge. Since Registries enforce metric uniqueness,
311318
// edges should be cached and re-used for each RPC.
312-
func newEdge(logger *zap.Logger, meter *metrics.Scope, tagToIgnore *metricsTagIgnore, req *transport.Request, direction string, rpcType transport.Type) *edge {
319+
func newEdge(ctx context.Context, logger *zap.Logger, sampledLogger *sampledlogger.SampledLogger, meter *metrics.Scope, tagToIgnore *metricsTagIgnore, decoratorTags []metricstagdecorator.MetricsTagDecorator, req *transport.Request, direction string, rpcType transport.Type) *edge {
313320
tags := tagToIgnore.tags(req, direction, rpcType)
314321

322+
// Merge custom decorator tags into the YARPC metrics base tag set.
323+
customDecoratorTags := getDecoratorTags(ctx, sampledLogger, decoratorTags)
324+
if customDecoratorTags != nil {
325+
for key, value := range *customDecoratorTags {
326+
tags[key] = value
327+
}
328+
}
329+
315330
// metrics for all RPCs
316331
calls, err := meter.Counter(metrics.Spec{
317332
Name: "calls",
@@ -603,3 +618,23 @@ func unknownIfEmpty(t string) string {
603618
}
604619
return t
605620
}
621+
622+
// getDecoratorTags collects tags from all provided MetricsTagDecorators.
623+
func getDecoratorTags(ctx context.Context, sampledLogger *sampledlogger.SampledLogger, metricsTagsDecorators []metricstagdecorator.MetricsTagDecorator) *metrics.Tags {
624+
if len(metricsTagsDecorators) == 0 {
625+
return nil
626+
}
627+
628+
decoratorTags := metrics.Tags{}
629+
for _, decorator := range metricsTagsDecorators {
630+
tags := decorator.ProvideTags(ctx)
631+
for key, value := range tags {
632+
if oldValue, exists := decoratorTags[key]; exists {
633+
sampledLogger.Warn("MetricsTagsDecorators is overwriting metric tag", zap.String("key", key), zap.String("old", oldValue), zap.String("new", value))
634+
}
635+
decoratorTags[key] = value
636+
}
637+
}
638+
639+
return &decoratorTags
640+
}

internal/observability/graph_test.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,27 @@ import (
2626

2727
"github.com/stretchr/testify/assert"
2828
"go.uber.org/net/metrics"
29+
"go.uber.org/yarpc/api/metrics/metricstagdecorator"
2930
"go.uber.org/yarpc/api/transport"
3031
"go.uber.org/yarpc/api/transport/transporttest"
32+
"go.uber.org/yarpc/internal/sampledlogger"
3133
"go.uber.org/zap"
3234
"go.uber.org/zap/zaptest/observer"
3335
)
3436

37+
const (
38+
testMetricsTagsDecoratorKey = "test_tag"
39+
testMetricsTagsDecoratorValue = "test_value"
40+
)
41+
42+
type TestMetricsTagsDecorator struct{}
43+
44+
func (d *TestMetricsTagsDecorator) ProvideTags(ctx context.Context) map[string]string {
45+
return map[string]string{
46+
testMetricsTagsDecoratorKey: testMetricsTagsDecoratorValue,
47+
}
48+
}
49+
3550
func TestHandleWithReservedField(t *testing.T) {
3651
root := metrics.New()
3752
meter := root.Scope()
@@ -148,6 +163,41 @@ func TestMetricsTagIgnore(t *testing.T) {
148163
}
149164
}
150165

166+
func TestPopulatingMetricsTagsDecorators(t *testing.T) {
167+
root := metrics.New()
168+
meter := root.Scope()
169+
req := &transport.Request{
170+
Caller: "caller",
171+
Service: "service",
172+
Transport: "",
173+
Encoding: "proto",
174+
Procedure: "procedure",
175+
RoutingKey: "rk",
176+
RoutingDelegate: "rd",
177+
}
178+
179+
// Creating new edge with MetricsTagsDecorators
180+
decorator := []metricstagdecorator.MetricsTagDecorator{&TestMetricsTagsDecorator{}}
181+
graph := newGraph(meter, zap.NewNop(), nil, nil, decorator)
182+
183+
_ = newEdge(context.Background(), graph.logger, graph.sampledLogger, graph.meter, graph.ignoreMetricsTag, graph.decoratorTags, req, string(_directionOutbound), transport.Unary)
184+
185+
for _, counter := range root.Snapshot().Counters {
186+
assert.Equal(t, counter.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
187+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", counter.Name)
188+
}
189+
190+
for _, gauge := range root.Snapshot().Gauges {
191+
assert.Equal(t, gauge.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
192+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", gauge.Name)
193+
}
194+
195+
for _, histogram := range root.Snapshot().Histograms {
196+
assert.Equal(t, histogram.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
197+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", histogram.Name)
198+
}
199+
}
200+
151201
func TestEdgeNopFallbacks(t *testing.T) {
152202
// If we fail to create any of the metrics required for the edge, we should
153203
// fall back to no-op implementations. The easiest way to trigger failures
@@ -166,11 +216,11 @@ func TestEdgeNopFallbacks(t *testing.T) {
166216
}
167217

168218
// Should succeed, covered by middleware tests.
169-
_ = newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, req, string(_directionOutbound), transport.Unary)
219+
_ = newEdge(context.Background(), zap.NewNop(), sampledlogger.NewDefaultSampledLogger(), meter, &metricsTagIgnore{}, nil, req, string(_directionOutbound), transport.Unary)
170220

171221
// Should fall back to no-op metrics.
172222
// Usage of nil metrics should not panic, should not observe changes.
173-
e := newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, req, string(_directionOutbound), transport.Unary)
223+
e := newEdge(context.Background(), zap.NewNop(), sampledlogger.NewDefaultSampledLogger(), meter, &metricsTagIgnore{}, nil, req, string(_directionOutbound), transport.Unary)
174224

175225
e.calls.Inc()
176226
assert.Equal(t, int64(0), e.calls.Load(), "Expected to fall back to no-op metrics.")

internal/observability/middleware.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"sync"
2626

2727
"go.uber.org/net/metrics"
28+
"go.uber.org/yarpc/api/metrics/metricstagdecorator"
2829
"go.uber.org/yarpc/api/transport"
2930
"go.uber.org/yarpc/yarpcerrors"
3031
"go.uber.org/zap"
@@ -99,6 +100,9 @@ type Config struct {
99100

100101
// Levels specify log levels for various classes of requests.
101102
Levels LevelsConfig
103+
104+
// MetricsTagDecorators populates yarpc metrics scope with custom tags.
105+
MetricsTagsDecorators []metricstagdecorator.MetricsTagDecorator
102106
}
103107

104108
// LevelsConfig specifies log level overrides for inbound traffic, outbound
@@ -147,7 +151,7 @@ type DirectionalLevelsConfig struct {
147151
// NewMiddleware constructs an observability middleware with the provided
148152
// configuration.
149153
func NewMiddleware(cfg Config) *Middleware {
150-
m := &Middleware{newGraph(cfg.Scope, cfg.Logger, cfg.ContextExtractor, cfg.MetricTagsBlocklist)}
154+
m := &Middleware{newGraph(cfg.Scope, cfg.Logger, cfg.ContextExtractor, cfg.MetricTagsBlocklist, cfg.MetricsTagsDecorators)}
151155

152156
// Apply the default levels
153157
applyLogLevelsConfig(&m.graph.inboundLevels, &cfg.Levels.Default)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package sampledlogger
2+
3+
import (
4+
"sync/atomic"
5+
"time"
6+
7+
"go.uber.org/zap"
8+
"go.uber.org/zap/zapcore"
9+
)
10+
11+
type SampledLogger struct {
12+
logger *zap.Logger
13+
logInterval time.Duration
14+
lastLogTime atomic.Value
15+
}
16+
17+
// NewSampledLogger creates a new SampledLogger with custom interval and zap.Logger.
18+
func NewSampledLogger(interval time.Duration, logger *zap.Logger) *SampledLogger {
19+
return &SampledLogger{
20+
logger: logger,
21+
logInterval: interval,
22+
}
23+
}
24+
25+
// NewDefaultSampledLogger creates a SampledLogger with zap.NewNop() and 5-minute interval.
26+
func NewDefaultSampledLogger() *SampledLogger {
27+
return NewSampledLogger(5*time.Minute, zap.NewNop())
28+
}
29+
30+
// log performs rate-limited logging for the given level and message.
31+
func (sl *SampledLogger) log(level zapcore.Level, msg string, fields ...zap.Field) {
32+
now := time.Now()
33+
last := sl.lastLogTime.Load().(time.Time)
34+
35+
if last.IsZero() || now.Sub(last) > sl.logInterval {
36+
switch level {
37+
case zapcore.DebugLevel:
38+
sl.logger.Debug(msg, fields...)
39+
case zapcore.InfoLevel:
40+
sl.logger.Info(msg, fields...)
41+
case zapcore.WarnLevel:
42+
sl.logger.Warn(msg, fields...)
43+
case zapcore.ErrorLevel:
44+
sl.logger.Error(msg, fields...)
45+
case zapcore.DPanicLevel:
46+
sl.logger.DPanic(msg, fields...)
47+
case zapcore.PanicLevel:
48+
sl.logger.Panic(msg, fields...)
49+
case zapcore.FatalLevel:
50+
sl.logger.Fatal(msg, fields...)
51+
}
52+
sl.lastLogTime.Store(now)
53+
}
54+
}
55+
56+
// Debug logs a debug-level message with rate limiting.
57+
func (sl *SampledLogger) Debug(msg string, fields ...zap.Field) {
58+
sl.log(zapcore.DebugLevel, msg, fields...)
59+
}
60+
61+
// Info logs an info-level message with rate limiting.
62+
func (sl *SampledLogger) Info(msg string, fields ...zap.Field) {
63+
sl.log(zapcore.InfoLevel, msg, fields...)
64+
}
65+
66+
// Warn logs a warn-level message with rate limiting.
67+
func (sl *SampledLogger) Warn(msg string, fields ...zap.Field) {
68+
sl.log(zapcore.WarnLevel, msg, fields...)
69+
}
70+
71+
// Error logs an error-level message with rate limiting.
72+
func (sl *SampledLogger) Error(msg string, fields ...zap.Field) {
73+
sl.log(zapcore.ErrorLevel, msg, fields...)
74+
}
75+
76+
// DPanic logs a DPanic-level message with rate limiting.
77+
func (sl *SampledLogger) DPanic(msg string, fields ...zap.Field) {
78+
sl.log(zapcore.DPanicLevel, msg, fields...)
79+
}
80+
81+
// Panic logs a panic-level message with rate limiting.
82+
func (sl *SampledLogger) Panic(msg string, fields ...zap.Field) {
83+
sl.log(zapcore.PanicLevel, msg, fields...)
84+
}
85+
86+
// Fatal logs a fatal-level message with rate limiting.
87+
func (sl *SampledLogger) Fatal(msg string, fields ...zap.Field) {
88+
sl.log(zapcore.FatalLevel, msg, fields...)
89+
}

0 commit comments

Comments
 (0)