Skip to content

Commit 33b6fdc

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

File tree

6 files changed

+121
-19
lines changed

6 files changed

+121
-19
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 metricstagdecorators
22+
23+
// DecoratorProperties is used for passing request-specific properties
24+
// to metrics decorators.
25+
type DecoratorProperties struct{}
26+
27+
// MetricsTagsDecorators is used for adding custom tags to YARPC metrics.
28+
type MetricsTagsDecorators interface {
29+
ProvideTags(properties DecoratorProperties) 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/metricstagdecorators"
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+
// MetricTagDecorators populates yarpc metrics scope with custom tags.
162+
MetricTagsDecorators []metricstagdecorators.MetricsTagsDecorators
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+
MetricTagsDecorators: cfg.Metrics.MetricTagsDecorators,
110111
Levels: observability.LevelsConfig{
111112
Default: observability.DirectionalLevelsConfig{
112113
Success: cfg.Logging.Levels.Success,

internal/observability/graph.go

Lines changed: 29 additions & 12 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/metricstagdecorators"
3031
"go.uber.org/yarpc/api/transport"
3132
"go.uber.org/yarpc/internal/digester"
3233
"go.uber.org/zap"
@@ -68,10 +69,11 @@ const (
6869
// A graph represents a collection of services: each service is a node, and we
6970
// collect stats for each caller-callee-transport-encoding-procedure-rk-sk-rd edge.
7071
type graph struct {
71-
meter *metrics.Scope
72-
logger *zap.Logger
73-
extract ContextExtractor
74-
ignoreMetricsTag *metricsTagIgnore
72+
meter *metrics.Scope
73+
logger *zap.Logger
74+
extract ContextExtractor
75+
ignoreMetricsTag *metricsTagIgnore
76+
metricTagsDecorators []metricstagdecorators.MetricsTagsDecorators
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, metricTagsDecorators []metricstagdecorators.MetricsTagsDecorators) graph {
168170
return graph{
169-
edges: make(map[string]*edge, _defaultGraphSize),
170-
meter: meter,
171-
logger: logger,
172-
extract: extract,
173-
ignoreMetricsTag: newMetricsTagIgnore(metricTagsIgnore),
171+
edges: make(map[string]*edge, _defaultGraphSize),
172+
meter: meter,
173+
logger: logger,
174+
extract: extract,
175+
ignoreMetricsTag: newMetricsTagIgnore(metricTagsIgnore),
176+
metricTagsDecorators: metricTagsDecorators,
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.metricTagsDecorators, req, direction, rpcType)
268271
g.edges[string(key)] = e
269272
return e
270273
}
@@ -309,9 +312,23 @@ 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, metricTagsDecorators []metricstagdecorators.MetricsTagsDecorators, req *transport.Request, direction string, rpcType transport.Type) *edge {
313316
tags := tagToIgnore.tags(req, direction, rpcType)
314317

318+
if len(metricTagsDecorators) > 0 {
319+
decoratorProperties := metricstagdecorators.DecoratorProperties{}
320+
321+
for _, decorator := range metricTagsDecorators {
322+
decoratorTags := decorator.ProvideTags(decoratorProperties)
323+
for key, value := range decoratorTags {
324+
if _, exists := tags[key]; exists {
325+
logger.Warn("MetricsTagsDecorators is overwriting metric tag", zap.String("key", key), zap.String("old", tags[key]), zap.String("new", value))
326+
}
327+
tags[key] = value
328+
}
329+
}
330+
}
331+
315332
// metrics for all RPCs
316333
calls, err := meter.Counter(metrics.Spec{
317334
Name: "calls",

internal/observability/graph_test.go

Lines changed: 49 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/metricstagdecorators"
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(request metricstagdecorators.DecoratorProperties) 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,39 @@ 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+
decorators := []metricstagdecorators.MetricsTagsDecorators{&TestMetricsTagsDecorator{}}
179+
180+
_ = newEdge(zap.NewNop(), meter, &metricsTagIgnore{}, decorators, req, string(_directionOutbound), transport.Unary)
181+
182+
for _, counter := range root.Snapshot().Counters {
183+
assert.Equal(t, counter.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
184+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", counter.Name)
185+
}
186+
187+
for _, gauge := range root.Snapshot().Gauges {
188+
assert.Equal(t, gauge.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
189+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", gauge.Name)
190+
}
191+
192+
for _, histogram := range root.Snapshot().Histograms {
193+
assert.Equal(t, histogram.Tags[testMetricsTagsDecoratorKey], testMetricsTagsDecoratorValue,
194+
"Expected testMetricsTagsDecorator tag to populate in %s metrics tags", histogram.Name)
195+
}
196+
}
197+
151198
func TestEdgeNopFallbacks(t *testing.T) {
152199
// If we fail to create any of the metrics required for the edge, we should
153200
// fall back to no-op implementations. The easiest way to trigger failures
@@ -166,11 +213,11 @@ func TestEdgeNopFallbacks(t *testing.T) {
166213
}
167214

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

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

175222
e.calls.Inc()
176223
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/metricstagdecorators"
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+
MetricTagsDecorators []metricstagdecorators.MetricsTagsDecorators
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.MetricTagsDecorators)}
151155

152156
// Apply the default levels
153157
applyLogLevelsConfig(&m.graph.inboundLevels, &cfg.Levels.Default)

0 commit comments

Comments
 (0)