Skip to content

Commit 2dc9821

Browse files
authored
feat: add ContextLabels to ClientMetrics (#798)
1 parent 2338d5a commit 2dc9821

File tree

5 files changed

+370
-54
lines changed

5 files changed

+370
-54
lines changed

examples/client/main.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func main() {
6161
grpcprom.WithClientHandlingTimeHistogram(
6262
grpcprom.WithHistogramBuckets([]float64{0.001, 0.01, 0.1, 0.3, 0.6, 1, 3, 6, 9, 20, 30, 60, 90, 120}),
6363
),
64+
// Add user_id as a context label. This user option is necessary
65+
// to initialize the metrics with the labels that will be provided
66+
// dynamically from the context. This should be used in tandem with
67+
// WithLabelsFromContext in the interceptor options.
68+
grpcprom.WithClientContextLabels("user_id"),
6469
)
6570
reg.MustRegister(clMetrics)
6671
exemplarFromContext := func(ctx context.Context) prometheus.Labels {
@@ -70,6 +75,21 @@ func main() {
7075
return nil
7176
}
7277

78+
// Extract the user id value from gRPC metadata
79+
// and use it as a label on our metrics.
80+
labelsFromContext := func(ctx context.Context) prometheus.Labels {
81+
labels := prometheus.Labels{}
82+
83+
md := metadata.ExtractOutgoing(ctx)
84+
userID := md.Get("user-id")
85+
if userID == "" {
86+
userID = "unknown"
87+
}
88+
labels["user_id"] = userID
89+
90+
return labels
91+
}
92+
7393
// Set up OTLP tracing (stdout for debug).
7494
exporter, err := stdout.New(stdout.WithPrettyPrint())
7595
if err != nil {
@@ -90,10 +110,16 @@ func main() {
90110
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
91111
grpc.WithChainUnaryInterceptor(
92112
timeout.UnaryClientInterceptor(500*time.Millisecond),
93-
clMetrics.UnaryClientInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)),
113+
clMetrics.UnaryClientInterceptor(
114+
grpcprom.WithExemplarFromContext(exemplarFromContext),
115+
grpcprom.WithLabelsFromContext(labelsFromContext),
116+
),
94117
logging.UnaryClientInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID))),
95118
grpc.WithChainStreamInterceptor(
96-
clMetrics.StreamClientInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)),
119+
clMetrics.StreamClientInterceptor(
120+
grpcprom.WithExemplarFromContext(exemplarFromContext),
121+
grpcprom.WithLabelsFromContext(labelsFromContext),
122+
),
97123
logging.StreamClientInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID))),
98124
)
99125
if err != nil {
@@ -113,7 +139,10 @@ func main() {
113139
case <-time.After(1 * time.Second):
114140
}
115141

116-
md := grpcMetadata.Pairs("authorization", "bearer yolo")
142+
md := grpcMetadata.Pairs(
143+
"authorization", "bearer yolo",
144+
"user-id", "admin",
145+
)
117146
if _, err := cl.Ping(metadata.MD(md).ToOutgoing(ctx), &testpb.PingRequest{Value: "example"}); err != nil {
118147
return err
119148
}

providers/prometheus/client_metrics.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,42 +23,60 @@ type ClientMetrics struct {
2323
clientStreamRecvHistogram *prometheus.HistogramVec
2424
// clientStreamSendHistogram can be nil
2525
clientStreamSendHistogram *prometheus.HistogramVec
26+
27+
// contextLabelNames stores the names of context labels
28+
contextLabelNames []string
2629
}
2730

2831
// NewClientMetrics returns a new ClientMetrics object.
2932
// NOTE: Remember to register ClientMetrics object using prometheus registry
3033
// e.g. prometheus.MustRegister(myClientMetrics).
3134
func NewClientMetrics(opts ...ClientMetricsOption) *ClientMetrics {
32-
var config clientMetricsConfig
35+
config := &clientMetricsConfig{
36+
clientHandledHistogramFn: func() *prometheus.HistogramVec { return nil },
37+
clientStreamRecvHistogramFn: func() *prometheus.HistogramVec { return nil },
38+
clientStreamSendHistogramFn: func() *prometheus.HistogramVec { return nil },
39+
}
3340
config.apply(opts)
41+
42+
// Build label names by combining default labels with context labels
43+
defaultLabels := []string{"grpc_type", "grpc_service", "grpc_method"}
44+
defaultLabelsWithCode := []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}
45+
46+
startedLabels := append(defaultLabels, config.contextLabels...)
47+
handledLabels := append(defaultLabelsWithCode, config.contextLabels...)
48+
streamLabels := append(defaultLabels, config.contextLabels...)
49+
3450
return &ClientMetrics{
3551
clientStartedCounter: prometheus.NewCounterVec(
3652
config.counterOpts.apply(prometheus.CounterOpts{
3753
Name: "grpc_client_started_total",
3854
Help: "Total number of RPCs started on the client.",
39-
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
55+
}), startedLabels),
4056

4157
clientHandledCounter: prometheus.NewCounterVec(
4258
config.counterOpts.apply(prometheus.CounterOpts{
4359
Name: "grpc_client_handled_total",
4460
Help: "Total number of RPCs completed by the client, regardless of success or failure.",
45-
}), []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}),
61+
}), handledLabels),
4662

4763
clientStreamMsgReceived: prometheus.NewCounterVec(
4864
config.counterOpts.apply(prometheus.CounterOpts{
4965
Name: "grpc_client_msg_received_total",
5066
Help: "Total number of RPC stream messages received by the client.",
51-
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
67+
}), streamLabels),
5268

5369
clientStreamMsgSent: prometheus.NewCounterVec(
5470
config.counterOpts.apply(prometheus.CounterOpts{
5571
Name: "grpc_client_msg_sent_total",
5672
Help: "Total number of gRPC stream messages sent by the client.",
57-
}), []string{"grpc_type", "grpc_service", "grpc_method"}),
73+
}), streamLabels),
74+
75+
clientHandledHistogram: config.clientHandledHistogramFn(),
76+
clientStreamRecvHistogram: config.clientStreamRecvHistogramFn(),
77+
clientStreamSendHistogram: config.clientStreamSendHistogramFn(),
5878

59-
clientHandledHistogram: config.clientHandledHistogram,
60-
clientStreamRecvHistogram: config.clientStreamRecvHistogram,
61-
clientStreamSendHistogram: config.clientStreamSendHistogram,
79+
contextLabelNames: config.contextLabels,
6280
}
6381
}
6482

providers/prometheus/client_options.go

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99

1010
type clientMetricsConfig struct {
1111
counterOpts counterOptions
12-
// clientHandledHistogram can be nil.
13-
clientHandledHistogram *prometheus.HistogramVec
14-
// clientStreamRecvHistogram can be nil.
15-
clientStreamRecvHistogram *prometheus.HistogramVec
16-
// clientStreamSendHistogram can be nil.
17-
clientStreamSendHistogram *prometheus.HistogramVec
12+
// clientHandledHistogramFn can be nil.
13+
clientHandledHistogramFn func() *prometheus.HistogramVec
14+
// clientStreamRecvHistogramFn can be nil.
15+
clientStreamRecvHistogramFn func() *prometheus.HistogramVec
16+
// clientStreamSendHistogramFn can be nil.
17+
clientStreamSendHistogramFn func() *prometheus.HistogramVec
18+
// contextLabels defines the names of dynamic labels to be extracted from context
19+
contextLabels []string
1820
}
1921

2022
type ClientMetricsOption func(*clientMetricsConfig)
@@ -35,43 +37,67 @@ func WithClientCounterOptions(opts ...CounterOption) ClientMetricsOption {
3537
// Histogram metrics can be very expensive for Prometheus to retain and query.
3638
func WithClientHandlingTimeHistogram(opts ...HistogramOption) ClientMetricsOption {
3739
return func(o *clientMetricsConfig) {
38-
o.clientHandledHistogram = prometheus.NewHistogramVec(
39-
histogramOptions(opts).apply(&prometheus.HistogramOpts{
40-
Name: "grpc_client_handling_seconds",
41-
Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.",
42-
Buckets: prometheus.DefBuckets,
43-
}),
44-
[]string{"grpc_type", "grpc_service", "grpc_method"},
45-
)
40+
o.clientHandledHistogramFn = func() *prometheus.HistogramVec {
41+
defaultLabels := []string{"grpc_type", "grpc_service", "grpc_method"}
42+
allLabels := append(defaultLabels, o.contextLabels...)
43+
44+
return prometheus.NewHistogramVec(
45+
histogramOptions(opts).apply(&prometheus.HistogramOpts{
46+
Name: "grpc_client_handling_seconds",
47+
Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.",
48+
Buckets: prometheus.DefBuckets,
49+
}),
50+
allLabels,
51+
)
52+
}
4653
}
4754
}
4855

4956
// WithClientStreamRecvHistogram turns on recording of single message receive time of streaming RPCs.
5057
// Histogram metrics can be very expensive for Prometheus to retain and query.
5158
func WithClientStreamRecvHistogram(opts ...HistogramOption) ClientMetricsOption {
5259
return func(o *clientMetricsConfig) {
53-
o.clientStreamRecvHistogram = prometheus.NewHistogramVec(
54-
histogramOptions(opts).apply(&prometheus.HistogramOpts{
55-
Name: "grpc_client_msg_recv_handling_seconds",
56-
Help: "Histogram of response latency (seconds) of the gRPC single message receive.",
57-
Buckets: prometheus.DefBuckets,
58-
}),
59-
[]string{"grpc_type", "grpc_service", "grpc_method"},
60-
)
60+
o.clientStreamRecvHistogramFn = func() *prometheus.HistogramVec {
61+
defaultLabels := []string{"grpc_type", "grpc_service", "grpc_method"}
62+
allLabels := append(defaultLabels, o.contextLabels...)
63+
64+
return prometheus.NewHistogramVec(
65+
histogramOptions(opts).apply(&prometheus.HistogramOpts{
66+
Name: "grpc_client_msg_recv_handling_seconds",
67+
Help: "Histogram of response latency (seconds) of the gRPC single message receive.",
68+
Buckets: prometheus.DefBuckets,
69+
}),
70+
allLabels,
71+
)
72+
}
6173
}
6274
}
6375

6476
// WithClientStreamSendHistogram turns on recording of single message send time of streaming RPCs.
6577
// Histogram metrics can be very expensive for Prometheus to retain and query.
6678
func WithClientStreamSendHistogram(opts ...HistogramOption) ClientMetricsOption {
6779
return func(o *clientMetricsConfig) {
68-
o.clientStreamSendHistogram = prometheus.NewHistogramVec(
69-
histogramOptions(opts).apply(&prometheus.HistogramOpts{
70-
Name: "grpc_client_msg_send_handling_seconds",
71-
Help: "Histogram of response latency (seconds) of the gRPC single message send.",
72-
Buckets: prometheus.DefBuckets,
73-
}),
74-
[]string{"grpc_type", "grpc_service", "grpc_method"},
75-
)
80+
o.clientStreamSendHistogramFn = func() *prometheus.HistogramVec {
81+
defaultLabels := []string{"grpc_type", "grpc_service", "grpc_method"}
82+
allLabels := append(defaultLabels, o.contextLabels...)
83+
84+
return prometheus.NewHistogramVec(
85+
histogramOptions(opts).apply(&prometheus.HistogramOpts{
86+
Name: "grpc_client_msg_send_handling_seconds",
87+
Help: "Histogram of response latency (seconds) of the gRPC single message send.",
88+
Buckets: prometheus.DefBuckets,
89+
}),
90+
allLabels,
91+
)
92+
}
93+
}
94+
}
95+
96+
// WithClientContextLabels configures the server metrics to include dynamic labels extracted from context.
97+
// The provided label names will be added to all server metrics as dynamic labels.
98+
// Use WithLabelsFromContext in the interceptor options to specify how to extract these labels from context.
99+
func WithClientContextLabels(labelNames ...string) ClientMetricsOption {
100+
return func(o *clientMetricsConfig) {
101+
o.contextLabels = labelNames
76102
}
77103
}

0 commit comments

Comments
 (0)