Skip to content

Commit cf7e69f

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

File tree

6 files changed

+119
-10
lines changed

6 files changed

+119
-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: 29 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,14 @@ 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 {
168170
return graph{
169171
edges: make(map[string]*edge, _defaultGraphSize),
170172
meter: meter,
171173
logger: logger,
172174
extract: extract,
173175
ignoreMetricsTag: newMetricsTagIgnore(metricTagsIgnore),
176+
decoratorTags: getDecoratorTags(logger, metricsTagsDecorators),
174177
inboundLevels: levels{
175178
success: zapcore.DebugLevel,
176179
failure: zapcore.ErrorLevel,
@@ -264,7 +267,7 @@ func (g *graph) createEdge(key []byte, req *transport.Request, direction string,
264267
return e
265268
}
266269

267-
e := newEdge(g.logger, g.meter, g.ignoreMetricsTag, req, direction, rpcType)
270+
e := newEdge(g.logger, g.meter, g.ignoreMetricsTag, g.decoratorTags, req, direction, rpcType)
268271
g.edges[string(key)] = e
269272
return e
270273
}
@@ -309,9 +312,14 @@ type streamEdge struct {
309312

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

318+
// Merge custom decorator tags into the YARPC metrics base tag set.
319+
for key, value := range decoratorTags {
320+
tags[key] = value
321+
}
322+
315323
// metrics for all RPCs
316324
calls, err := meter.Counter(metrics.Spec{
317325
Name: "calls",
@@ -603,3 +611,21 @@ func unknownIfEmpty(t string) string {
603611
}
604612
return t
605613
}
614+
615+
// getDecoratorTags collects tags from all provided MetricsTagDecorators.
616+
func getDecoratorTags(logger *zap.Logger, metricsTagsDecorators []metricstagdecorator.MetricsTagDecorator) metrics.Tags {
617+
decoratorTags := metrics.Tags{}
618+
if len(metricsTagsDecorators) > 0 {
619+
for _, decorator := range metricsTagsDecorators {
620+
tags := decorator.ProvideTags()
621+
for key, value := range tags {
622+
if oldValue, exists := decoratorTags[key]; exists {
623+
logger.Warn("MetricsTagsDecorators is overwriting metric tag", zap.String("key", key), zap.String("old", oldValue), zap.String("new", value))
624+
}
625+
decoratorTags[key] = value
626+
}
627+
}
628+
}
629+
630+
return decoratorTags
631+
}

internal/observability/graph_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,26 @@ 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"
3132
"go.uber.org/zap"
3233
"go.uber.org/zap/zaptest/observer"
3334
)
3435

36+
const (
37+
testMetricsTagsDecoratorKey = "test_tag"
38+
testMetricsTagsDecoratorValue = "test_value"
39+
)
40+
41+
type TestMetricsTagsDecorator struct{}
42+
43+
func (d *TestMetricsTagsDecorator) ProvideTags() map[string]string {
44+
return map[string]string{
45+
testMetricsTagsDecoratorKey: testMetricsTagsDecoratorValue,
46+
}
47+
}
48+
3549
func TestHandleWithReservedField(t *testing.T) {
3650
root := metrics.New()
3751
meter := root.Scope()
@@ -148,6 +162,41 @@ func TestMetricsTagIgnore(t *testing.T) {
148162
}
149163
}
150164

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

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

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

175224
e.calls.Inc()
176225
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)