Skip to content

Commit 4e21cee

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
1 parent f81211d commit 4e21cee

File tree

6 files changed

+114
-10
lines changed

6 files changed

+114
-10
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
// MetricsTagDecorator is used for adding custom tags to YARPC metrics.
24+
type MetricsTagDecorator interface {
25+
ProvideTags() map[string]string
26+
}

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: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ 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"
3233
"go.uber.org/zap"
@@ -72,6 +73,7 @@ type graph struct {
7273
logger *zap.Logger
7374
extract ContextExtractor
7475
ignoreMetricsTag *metricsTagIgnore
76+
decoratorTags metrics.Tags
7577

7678
edgesMu sync.RWMutex
7779
edges map[string]*edge
@@ -164,13 +166,27 @@ func (m *metricsTagIgnore) tags(req *transport.Request, direction string, rpcTyp
164166
return tags
165167
}
166168

167-
func newGraph(meter *metrics.Scope, logger *zap.Logger, extract ContextExtractor, metricTagsIgnore []string) graph {
169+
func newGraph(meter *metrics.Scope, logger *zap.Logger, extract ContextExtractor, metricTagsIgnore []string, metricsTagsDecorators []metricstagdecorator.MetricsTagDecorator) graph {
170+
decoratorTags := metrics.Tags{}
171+
if len(metricsTagsDecorators) > 0 {
172+
for _, decorator := range metricsTagsDecorators {
173+
tags := decorator.ProvideTags()
174+
for key, value := range tags {
175+
if oldValue, exists := decoratorTags[key]; exists {
176+
logger.Warn("MetricsTagsDecorators is overwriting metric tag", zap.String("key", key), zap.String("old", oldValue), zap.String("new", value))
177+
}
178+
decoratorTags[key] = value
179+
}
180+
}
181+
}
182+
168183
return graph{
169184
edges: make(map[string]*edge, _defaultGraphSize),
170185
meter: meter,
171186
logger: logger,
172187
extract: extract,
173188
ignoreMetricsTag: newMetricsTagIgnore(metricTagsIgnore),
189+
decoratorTags: decoratorTags,
174190
inboundLevels: levels{
175191
success: zapcore.DebugLevel,
176192
failure: zapcore.ErrorLevel,
@@ -264,7 +280,7 @@ func (g *graph) createEdge(key []byte, req *transport.Request, direction string,
264280
return e
265281
}
266282

267-
e := newEdge(g.logger, g.meter, g.ignoreMetricsTag, req, direction, rpcType)
283+
e := newEdge(g.logger, g.meter, g.ignoreMetricsTag, g.decoratorTags, req, direction, rpcType)
268284
g.edges[string(key)] = e
269285
return e
270286
}
@@ -309,9 +325,15 @@ type streamEdge struct {
309325

310326
// newEdge constructs a new edge. Since Registries enforce metric uniqueness,
311327
// 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 {
328+
func newEdge(logger *zap.Logger, meter *metrics.Scope, tagToIgnore *metricsTagIgnore, decoratorTags metrics.Tags, req *transport.Request, direction string, rpcType transport.Type) *edge {
313329
tags := tagToIgnore.tags(req, direction, rpcType)
314330

331+
if len(decoratorTags) > 0 {
332+
for key, value := range decoratorTags {
333+
tags[key] = value
334+
}
335+
}
336+
315337
// metrics for all RPCs
316338
calls, err := meter.Counter(metrics.Spec{
317339
Name: "calls",

internal/observability/graph_test.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ import (
3232
"go.uber.org/zap/zaptest/observer"
3333
)
3434

35+
const (
36+
testMetricsTagsDecoratorKey = "test_tag"
37+
testMetricsTagsDecoratorValue = "test_value"
38+
)
39+
40+
type TestMetricsTagsDecorator struct{}
41+
42+
func (d *TestMetricsTagsDecorator) ProvideTags() map[string]string {
43+
return map[string]string{
44+
testMetricsTagsDecoratorKey: testMetricsTagsDecoratorValue,
45+
}
46+
}
47+
3548
func TestHandleWithReservedField(t *testing.T) {
3649
root := metrics.New()
3750
meter := root.Scope()
@@ -148,6 +161,41 @@ func TestMetricsTagIgnore(t *testing.T) {
148161
}
149162
}
150163

164+
func TestPopulatingMetricsTagsDecorators(t *testing.T) {
165+
root := metrics.New()
166+
meter := root.Scope()
167+
req := &transport.Request{
168+
Caller: "caller",
169+
Service: "service",
170+
Transport: "",
171+
Encoding: "proto",
172+
Procedure: "procedure",
173+
RoutingKey: "rk",
174+
RoutingDelegate: "rd",
175+
}
176+
177+
decoratorTags := metrics.Tags{
178+
testMetricsTagsDecoratorKey: testMetricsTagsDecoratorValue,
179+
}
180+
181+
_ = newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, decoratorTags, req, string(_directionOutbound), transport.Unary)
182+
183+
for _, counter := range root.Snapshot().Counters {
184+
assert.Equal(t, counter.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
185+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", counter.Name)
186+
}
187+
188+
for _, gauge := range root.Snapshot().Gauges {
189+
assert.Equal(t, gauge.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
190+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", gauge.Name)
191+
}
192+
193+
for _, histogram := range root.Snapshot().Histograms {
194+
assert.Equal(t, histogram.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
195+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", histogram.Name)
196+
}
197+
}
198+
151199
func TestEdgeNopFallbacks(t *testing.T) {
152200
// If we fail to create any of the metrics required for the edge, we should
153201
// fall back to no-op implementations. The easiest way to trigger failures
@@ -166,11 +214,11 @@ func TestEdgeNopFallbacks(t *testing.T) {
166214
}
167215

168216
// Should succeed, covered by middleware tests.
169-
_ = newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, req, string(_directionOutbound), transport.Unary)
217+
_ = newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, nil, req, string(_directionOutbound), transport.Unary)
170218

171219
// Should fall back to no-op metrics.
172220
// Usage of nil metrics should not panic, should not observe changes.
173-
e := newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, req, string(_directionOutbound), transport.Unary)
221+
e := newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, nil, req, string(_directionOutbound), transport.Unary)
174222

175223
e.calls.Inc()
176224
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)

0 commit comments

Comments
 (0)