You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2026/08/16 04:22:07 UTC

(dubbo-go) branch develop updated: docs(metrics): improve comments and test coverage of metrics options and rpc error classifier (#3665)

This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new a31acfb67 docs(metrics): improve comments and test coverage of metrics options and rpc error classifier (#3665)
a31acfb67 is described below

commit a31acfb67e9feb0694bd11cbfaf24689aec9146a
Author: Yu <zh...@gmail.com>
AuthorDate: Sun Aug 16 12:22:02 2026 +0800

    docs(metrics): improve comments and test coverage of metrics options and rpc error classifier (#3665)
    
    * chore(metrics): add godoc comments and tests for metrics options and rpc error classifier
---
 metrics/options.go                            | 119 ++++++++-
 metrics/options_test.go                       | 349 ++++++++++++++++++--------
 metrics/rpc/error_classifier.go               |   2 +
 metrics/rpc/error_classifier_test.go          | 133 ++++++++++
 protocol/triple/triple_protocol/code.go       |   6 +-
 protocol/triple/triple_protocol/error_test.go |   5 +
 6 files changed, 503 insertions(+), 111 deletions(-)

diff --git a/metrics/options.go b/metrics/options.go
index d494e6a33..6807114bc 100644
--- a/metrics/options.go
+++ b/metrics/options.go
@@ -26,14 +26,29 @@ import (
 	"dubbo.apache.org/dubbo-go/v3/global"
 )
 
+// Options holds the configuration of the metrics module.
+// It wraps the global MetricsConfig, and can be built programmatically
+// with NewOptions and a set of Option functions.
+//
+// The metrics module is disabled by default; use WithEnabled to turn it on.
+//
+// NewOptions returns zero values for untouched fields; the defaults
+// documented on the With* functions (e.g. port 9090, path "/metrics") only
+// take effect at instance (client/server) initialization.
 type Options struct {
 	Metrics *global.MetricsConfig
 }
 
+// defaultOptions wraps a fresh global.MetricsConfig; no option is applied,
+// so every field keeps its zero value until instance initialization.
 func defaultOptions() *Options {
 	return &Options{Metrics: global.DefaultMetricsConfig()}
 }
 
+// NewOptions wraps a fresh global.MetricsConfig and applies the given
+// options in order; later options win on conflicting fields. Fields left
+// untouched are zero-valued until filled with the defaults during instance
+// (client/server) initialization.
 func NewOptions(opts ...Option) *Options {
 	MetricOptions := defaultOptions()
 	for _, opt := range opts {
@@ -42,8 +57,13 @@ func NewOptions(opts ...Option) *Options {
 	return MetricOptions
 }
 
+// Option is a functional option used to customize the metrics Options.
 type Option func(*Options)
 
+// WithAggregationEnabled enables metrics aggregation, such as the
+// time-window based aggregation for counters and rt metrics.
+//
+// Aggregation is disabled by default (applied at instance initialization).
 func WithAggregationEnabled() Option {
 	return func(opts *Options) {
 		enabled := true
@@ -51,24 +71,44 @@ func WithAggregationEnabled() Option {
 	}
 }
 
+// WithAggregationBucketNum sets the number of buckets used by metrics
+// aggregation. A larger bucket count gives finer time resolution inside
+// the time window at the cost of more memory.
+//
+// The default (10) is applied at instance initialization.
 func WithAggregationBucketNum(num int) Option {
 	return func(opts *Options) {
 		opts.Metrics.Aggregation.BucketNum = num
 	}
 }
 
+// WithAggregationTimeWindowSeconds sets the time window, in seconds,
+// of the metrics aggregation. Metrics older than the window are
+// discarded from the aggregation result.
+//
+// The default (120 seconds) is applied at instance initialization.
 func WithAggregationTimeWindowSeconds(seconds int) Option {
 	return func(opts *Options) {
 		opts.Metrics.Aggregation.TimeWindowSeconds = seconds
 	}
 }
 
+// WithPrometheus sets the metrics protocol to prometheus.
+//
+// Prometheus is the default protocol (applied at instance initialization),
+// so this option is only needed to switch back to it after another
+// protocol has been configured.
 func WithPrometheus() Option {
 	return func(opts *Options) {
 		opts.Metrics.Protocol = "prometheus"
 	}
 }
 
+// WithPrometheusExporterEnabled enables the prometheus exporter, which
+// exposes the collected metrics over the http endpoint configured by
+// WithPort and WithPath.
+//
+// The exporter is enabled by default (applied at instance initialization).
 func WithPrometheusExporterEnabled() Option {
 	return func(opts *Options) {
 		enabled := true
@@ -76,6 +116,11 @@ func WithPrometheusExporterEnabled() Option {
 	}
 }
 
+// WithPrometheusPushgatewayEnabled enables pushing metrics to the
+// prometheus pushgateway, so that they can be scraped by prometheus
+// even if the instance is short-lived or unreachable directly.
+//
+// Pushgateway is disabled by default (applied at instance initialization).
 func WithPrometheusPushgatewayEnabled() Option {
 	return func(opts *Options) {
 		enabled := true
@@ -83,35 +128,64 @@ func WithPrometheusPushgatewayEnabled() Option {
 	}
 }
 
+// WithPrometheusGatewayUrl sets the base url of the prometheus
+// pushgateway, e.g. "http://pushgateway:9091".
+//
+// The option has no default value: it is not filled in at instance
+// initialization and must be set when pushgateway is enabled.
 func WithPrometheusGatewayUrl(url string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Prometheus.Pushgateway.BaseUrl = url
 	}
 }
 
+// WithPrometheusGatewayJob sets the job name reported to the prometheus
+// pushgateway. It is used to group the pushed metrics in prometheus.
+//
+// The default ("default_dubbo_job") is applied at instance initialization.
 func WithPrometheusGatewayJob(job string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Prometheus.Pushgateway.Job = job
 	}
 }
 
+// WithPrometheusGatewayUsername sets the username for basic
+// authentication with the prometheus pushgateway.
+//
+// The default (empty username, i.e. no authentication) is applied at
+// instance initialization.
 func WithPrometheusGatewayUsername(username string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Prometheus.Pushgateway.Username = username
 	}
 }
 
+// WithPrometheusGatewayPassword sets the password for basic
+// authentication with the prometheus pushgateway.
+//
+// The default (empty password, i.e. no authentication) is applied at
+// instance initialization.
 func WithPrometheusGatewayPassword(password string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Prometheus.Pushgateway.Password = password
 	}
 }
+
+// WithPrometheusGatewayInterval sets the interval at which metrics are
+// pushed to the prometheus pushgateway.
+//
+// The default (30 seconds) is applied at instance initialization.
 func WithPrometheusGatewayInterval(interval time.Duration) Option {
 	return func(opts *Options) {
 		opts.Metrics.Prometheus.Pushgateway.PushInterval = int(interval.Seconds())
 	}
 }
 
+// WithConfigCenterEnabled enables the config-center metrics, which
+// report the state of the dynamic configuration center (e.g. the
+// configuration that the instance has loaded or subscribed).
+//
+// Config-center metrics are disabled by default (applied at instance initialization).
 func WithConfigCenterEnabled() Option {
 	return func(opts *Options) {
 		b := true
@@ -119,6 +193,10 @@ func WithConfigCenterEnabled() Option {
 	}
 }
 
+// WithMetadataEnabled enables the metadata metrics, which report the
+// operations of the metadata center (e.g. store provider metadata).
+//
+// Metadata metrics are disabled by default (applied at instance initialization).
 func WithMetadataEnabled() Option {
 	return func(opts *Options) {
 		b := true
@@ -126,6 +204,10 @@ func WithMetadataEnabled() Option {
 	}
 }
 
+// WithRegistryEnabled enables the registry metrics, which report the
+// interactions with the service registry (e.g. register, subscribe).
+//
+// Registry metrics are disabled by default (applied at instance initialization).
 func WithRegistryEnabled() Option {
 	return func(opts *Options) {
 		b := true
@@ -133,7 +215,12 @@ func WithRegistryEnabled() Option {
 	}
 }
 
-// WithEnabled this will enable rpc and tracing by default, config-center, metadata and registry metrics will still be in disable state.
+// WithEnabled enables the metrics module. It enables the rpc metrics
+// by default, while config-center, metadata and registry metrics
+// are still in disable state and need WithConfigCenterEnabled,
+// WithMetadataEnabled and WithRegistryEnabled respectively.
+//
+// The metrics module is disabled by default (applied at instance initialization).
 func WithEnabled() Option {
 	return func(opts *Options) {
 		b := true
@@ -141,19 +228,29 @@ func WithEnabled() Option {
 	}
 }
 
+// WithPort sets the port on which the metrics are exposed.
+//
+// The default (9090) is applied at instance initialization.
 func WithPort(port int) Option {
 	return func(opts *Options) {
 		opts.Metrics.Port = strconv.Itoa(port)
 	}
 }
 
+// WithPath sets the http path on which the metrics are exposed.
+//
+// The default ("/metrics") is applied at instance initialization.
 func WithPath(path string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Path = path
 	}
 }
 
-// Below are options for probe
+// WithProbeEnabled enables the health probe endpoints
+// (liveness, readiness and startup), which are typically used
+// by Kubernetes for container health checks.
+//
+// Probe endpoints are disabled by default (applied at instance initialization).
 func WithProbeEnabled() Option {
 	return func(opts *Options) {
 		b := true
@@ -161,30 +258,48 @@ func WithProbeEnabled() Option {
 	}
 }
 
+// WithProbePort sets the port on which the probe endpoints are served.
+//
+// The default (22222) is applied at instance initialization.
 func WithProbePort(port int) Option {
 	return func(opts *Options) {
 		opts.Metrics.Probe.Port = strconv.Itoa(port)
 	}
 }
 
+// WithProbeLivenessPath sets the http path of the liveness probe.
+//
+// The default ("/live") is applied at instance initialization.
 func WithProbeLivenessPath(path string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Probe.LivenessPath = path
 	}
 }
 
+// WithProbeReadinessPath sets the http path of the readiness probe.
+//
+// The default ("/ready") is applied at instance initialization.
 func WithProbeReadinessPath(path string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Probe.ReadinessPath = path
 	}
 }
 
+// WithProbeStartupPath sets the http path of the startup probe.
+//
+// The default ("/startup") is applied at instance initialization.
 func WithProbeStartupPath(path string) Option {
 	return func(opts *Options) {
 		opts.Metrics.Probe.StartupPath = path
 	}
 }
 
+// WithProbeUseInternalState sets whether the probe endpoints report the
+// internal state of the framework (e.g. whether the dubbo server has
+// started up and is ready to serve) as the probe result, instead of
+// answering only with the result of the registered custom checks.
+//
+// The default (true) is applied at instance initialization.
 func WithProbeUseInternalState(use bool) Option {
 	return func(opts *Options) {
 		opts.Metrics.Probe.UseInternalState = &use
diff --git a/metrics/options_test.go b/metrics/options_test.go
index 7f7d2851a..ce5b662f6 100644
--- a/metrics/options_test.go
+++ b/metrics/options_test.go
@@ -30,122 +30,257 @@ func TestDefaultOptions(t *testing.T) {
 	opts := defaultOptions()
 	assert.NotNil(t, opts)
 	assert.NotNil(t, opts.Metrics)
+	// the metrics module and all sub-modules are disabled by default
+	assert.Nil(t, opts.Metrics.Enable)
+	assert.Nil(t, opts.Metrics.Aggregation.Enabled)
+	assert.Nil(t, opts.Metrics.Probe.Enabled)
+	assert.Nil(t, opts.Metrics.Prometheus.Pushgateway.Enabled)
+	assert.Empty(t, opts.Metrics.Port)
+	assert.Empty(t, opts.Metrics.Path)
+	assert.Empty(t, opts.Metrics.Protocol)
 }
 
 func TestNewOptions(t *testing.T) {
-	t.Run("no options", func(t *testing.T) {
-		opts := NewOptions()
-		assert.NotNil(t, opts)
-		assert.NotNil(t, opts.Metrics)
-	})
-
-	t.Run("with single option", func(t *testing.T) {
-		opts := NewOptions(WithPrometheus())
-		assert.NotNil(t, opts)
-		assert.Equal(t, "prometheus", opts.Metrics.Protocol)
-	})
-
-	t.Run("with multiple options", func(t *testing.T) {
-		opts := NewOptions(
-			WithPrometheus(),
-			WithPort(9090),
-			WithPath("/metrics"),
-		)
-		assert.NotNil(t, opts)
-		assert.Equal(t, "prometheus", opts.Metrics.Protocol)
-		assert.Equal(t, "9090", opts.Metrics.Port)
-		assert.Equal(t, "/metrics", opts.Metrics.Path)
-	})
-}
-
-func TestWithAggregationEnabled(t *testing.T) {
-	opts := NewOptions(WithAggregationEnabled())
-	assert.NotNil(t, opts.Metrics.Aggregation.Enabled)
-	assert.True(t, *opts.Metrics.Aggregation.Enabled)
-}
-
-func TestWithAggregationBucketNum(t *testing.T) {
-	opts := NewOptions(WithAggregationBucketNum(20))
-	assert.Equal(t, 20, opts.Metrics.Aggregation.BucketNum)
-}
-
-func TestWithAggregationTimeWindowSeconds(t *testing.T) {
-	opts := NewOptions(WithAggregationTimeWindowSeconds(60))
-	assert.Equal(t, 60, opts.Metrics.Aggregation.TimeWindowSeconds)
-}
-
-func TestWithPrometheus(t *testing.T) {
-	opts := NewOptions(WithPrometheus())
-	assert.Equal(t, "prometheus", opts.Metrics.Protocol)
-}
-
-func TestWithPrometheusExporterEnabled(t *testing.T) {
-	opts := NewOptions(WithPrometheusExporterEnabled())
-	assert.NotNil(t, opts.Metrics.Prometheus.Exporter.Enabled)
-	assert.True(t, *opts.Metrics.Prometheus.Exporter.Enabled)
-}
-
-func TestWithPrometheusPushgatewayEnabled(t *testing.T) {
-	opts := NewOptions(WithPrometheusPushgatewayEnabled())
-	assert.NotNil(t, opts.Metrics.Prometheus.Pushgateway.Enabled)
-	assert.True(t, *opts.Metrics.Prometheus.Pushgateway.Enabled)
-}
-
-func TestWithPrometheusGatewayUrl(t *testing.T) {
-	opts := NewOptions(WithPrometheusGatewayUrl("http://localhost:9091"))
-	assert.Equal(t, "http://localhost:9091", opts.Metrics.Prometheus.Pushgateway.BaseUrl)
-}
-
-func TestWithPrometheusGatewayJob(t *testing.T) {
-	opts := NewOptions(WithPrometheusGatewayJob("test-job"))
-	assert.Equal(t, "test-job", opts.Metrics.Prometheus.Pushgateway.Job)
-}
-
-func TestWithPrometheusGatewayUsername(t *testing.T) {
-	opts := NewOptions(WithPrometheusGatewayUsername("admin"))
-	assert.Equal(t, "admin", opts.Metrics.Prometheus.Pushgateway.Username)
-}
-
-func TestWithPrometheusGatewayPassword(t *testing.T) {
-	opts := NewOptions(WithPrometheusGatewayPassword("secret"))
-	assert.Equal(t, "secret", opts.Metrics.Prometheus.Pushgateway.Password)
-}
-
-func TestWithPrometheusGatewayInterval(t *testing.T) {
-	opts := NewOptions(WithPrometheusGatewayInterval(60 * time.Second))
-	assert.Equal(t, 60, opts.Metrics.Prometheus.Pushgateway.PushInterval)
-}
-
-func TestWithConfigCenterEnabled(t *testing.T) {
-	opts := NewOptions(WithConfigCenterEnabled())
-	assert.NotNil(t, opts.Metrics.EnableConfigCenter)
-	assert.True(t, *opts.Metrics.EnableConfigCenter)
-}
-
-func TestWithMetadataEnabled(t *testing.T) {
-	opts := NewOptions(WithMetadataEnabled())
-	assert.NotNil(t, opts.Metrics.EnableMetadata)
-	assert.True(t, *opts.Metrics.EnableMetadata)
+	tests := []struct {
+		name    string
+		options []Option
+		check   func(*testing.T, *Options)
+	}{
+		{
+			name: "no options",
+			check: func(t *testing.T, opts *Options) {
+				assert.NotNil(t, opts)
+				assert.NotNil(t, opts.Metrics)
+			},
+		},
+		{
+			name:    "single option",
+			options: []Option{WithPrometheus()},
+			check: func(t *testing.T, opts *Options) {
+				assert.Equal(t, "prometheus", opts.Metrics.Protocol)
+			},
+		},
+		{
+			name:    "multiple options",
+			options: []Option{WithPrometheus(), WithPort(9090), WithPath("/metrics")},
+			check: func(t *testing.T, opts *Options) {
+				assert.Equal(t, "prometheus", opts.Metrics.Protocol)
+				assert.Equal(t, "9090", opts.Metrics.Port)
+				assert.Equal(t, "/metrics", opts.Metrics.Path)
+			},
+		},
+		{
+			name:    "later option wins",
+			options: []Option{WithPort(8080), WithPort(9090)},
+			check: func(t *testing.T, opts *Options) {
+				assert.Equal(t, "9090", opts.Metrics.Port)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts := NewOptions(tt.options...)
+			tt.check(t, opts)
+		})
+	}
 }
 
-func TestWithRegistryEnabled(t *testing.T) {
-	opts := NewOptions(WithRegistryEnabled())
-	assert.NotNil(t, opts.Metrics.EnableRegistry)
-	assert.True(t, *opts.Metrics.EnableRegistry)
+func TestBoolFlagOptions(t *testing.T) {
+	tests := []struct {
+		name   string
+		option Option
+		get    func(*Options) *bool
+	}{
+		{
+			name:   "WithAggregationEnabled",
+			option: WithAggregationEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.Aggregation.Enabled },
+		},
+		{
+			name:   "WithPrometheusExporterEnabled",
+			option: WithPrometheusExporterEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.Prometheus.Exporter.Enabled },
+		},
+		{
+			name:   "WithPrometheusPushgatewayEnabled",
+			option: WithPrometheusPushgatewayEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.Prometheus.Pushgateway.Enabled },
+		},
+		{
+			name:   "WithConfigCenterEnabled",
+			option: WithConfigCenterEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.EnableConfigCenter },
+		},
+		{
+			name:   "WithMetadataEnabled",
+			option: WithMetadataEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.EnableMetadata },
+		},
+		{
+			name:   "WithRegistryEnabled",
+			option: WithRegistryEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.EnableRegistry },
+		},
+		{
+			name:   "WithEnabled",
+			option: WithEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.Enable },
+		},
+		{
+			name:   "WithProbeEnabled",
+			option: WithProbeEnabled(),
+			get:    func(o *Options) *bool { return o.Metrics.Probe.Enabled },
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts := NewOptions(tt.option)
+			assert.NotNil(t, opts.Metrics)
+			got := tt.get(opts)
+			assert.NotNil(t, got)
+			assert.True(t, *got)
+		})
+	}
 }
 
-func TestWithEnabled(t *testing.T) {
-	opts := NewOptions(WithEnabled())
-	assert.NotNil(t, opts.Metrics.Enable)
-	assert.True(t, *opts.Metrics.Enable)
+func TestStringFieldOptions(t *testing.T) {
+	tests := []struct {
+		name   string
+		option Option
+		want   string
+		get    func(*Options) string
+	}{
+		{
+			name:   "WithPrometheus",
+			option: WithPrometheus(),
+			want:   "prometheus",
+			get:    func(o *Options) string { return o.Metrics.Protocol },
+		},
+		{
+			name:   "WithPrometheusGatewayUrl",
+			option: WithPrometheusGatewayUrl("http://localhost:9091"),
+			want:   "http://localhost:9091",
+			get:    func(o *Options) string { return o.Metrics.Prometheus.Pushgateway.BaseUrl },
+		},
+		{
+			name:   "WithPrometheusGatewayJob",
+			option: WithPrometheusGatewayJob("test-job"),
+			want:   "test-job",
+			get:    func(o *Options) string { return o.Metrics.Prometheus.Pushgateway.Job },
+		},
+		{
+			name:   "WithPrometheusGatewayUsername",
+			option: WithPrometheusGatewayUsername("admin"),
+			want:   "admin",
+			get:    func(o *Options) string { return o.Metrics.Prometheus.Pushgateway.Username },
+		},
+		{
+			name:   "WithPrometheusGatewayPassword",
+			option: WithPrometheusGatewayPassword("secret"),
+			want:   "secret",
+			get:    func(o *Options) string { return o.Metrics.Prometheus.Pushgateway.Password },
+		},
+		{
+			name:   "WithPort",
+			option: WithPort(8080),
+			want:   "8080",
+			get:    func(o *Options) string { return o.Metrics.Port },
+		},
+		{
+			name:   "WithPath",
+			option: WithPath("/custom/metrics"),
+			want:   "/custom/metrics",
+			get:    func(o *Options) string { return o.Metrics.Path },
+		},
+		{
+			name:   "WithProbePort",
+			option: WithProbePort(12345),
+			want:   "12345",
+			get:    func(o *Options) string { return o.Metrics.Probe.Port },
+		},
+		{
+			name:   "WithProbeLivenessPath",
+			option: WithProbeLivenessPath("/custom/live"),
+			want:   "/custom/live",
+			get:    func(o *Options) string { return o.Metrics.Probe.LivenessPath },
+		},
+		{
+			name:   "WithProbeReadinessPath",
+			option: WithProbeReadinessPath("/custom/ready"),
+			want:   "/custom/ready",
+			get:    func(o *Options) string { return o.Metrics.Probe.ReadinessPath },
+		},
+		{
+			name:   "WithProbeStartupPath",
+			option: WithProbeStartupPath("/custom/startup"),
+			want:   "/custom/startup",
+			get:    func(o *Options) string { return o.Metrics.Probe.StartupPath },
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts := NewOptions(tt.option)
+			assert.Equal(t, tt.want, tt.get(opts))
+		})
+	}
 }
 
-func TestWithPort(t *testing.T) {
-	opts := NewOptions(WithPort(8080))
-	assert.Equal(t, "8080", opts.Metrics.Port)
+func TestIntFieldOptions(t *testing.T) {
+	tests := []struct {
+		name   string
+		option Option
+		want   int
+		get    func(*Options) int
+	}{
+		{
+			name:   "WithAggregationBucketNum",
+			option: WithAggregationBucketNum(20),
+			want:   20,
+			get:    func(o *Options) int { return o.Metrics.Aggregation.BucketNum },
+		},
+		{
+			name:   "WithAggregationTimeWindowSeconds",
+			option: WithAggregationTimeWindowSeconds(60),
+			want:   60,
+			get:    func(o *Options) int { return o.Metrics.Aggregation.TimeWindowSeconds },
+		},
+		{
+			name:   "WithPrometheusGatewayInterval",
+			option: WithPrometheusGatewayInterval(60 * time.Second),
+			want:   60,
+			get:    func(o *Options) int { return o.Metrics.Prometheus.Pushgateway.PushInterval },
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts := NewOptions(tt.option)
+			assert.Equal(t, tt.want, tt.get(opts))
+		})
+	}
 }
 
-func TestWithPath(t *testing.T) {
-	opts := NewOptions(WithPath("/custom/metrics"))
-	assert.Equal(t, "/custom/metrics", opts.Metrics.Path)
+func TestWithProbeUseInternalState(t *testing.T) {
+	tests := []struct {
+		name string
+		use  bool
+		want bool
+	}{
+		{
+			name: "use internal state",
+			use:  true,
+			want: true,
+		},
+		{
+			name: "not use internal state",
+			use:  false,
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts := NewOptions(WithProbeUseInternalState(tt.use))
+			assert.NotNil(t, opts.Metrics.Probe.UseInternalState)
+			assert.Equal(t, tt.want, *opts.Metrics.Probe.UseInternalState)
+		})
+	}
 }
diff --git a/metrics/rpc/error_classifier.go b/metrics/rpc/error_classifier.go
index bc87dd9b3..2a63409f1 100644
--- a/metrics/rpc/error_classifier.go
+++ b/metrics/rpc/error_classifier.go
@@ -102,6 +102,8 @@ const (
 // [ErrorTypeServiceUnavailable]. The application-layer CodeBizError maps to
 // [ErrorTypeBusinessFailed]. Plain Go errors (those that do not carry a Triple
 // status code) and every unhandled code fall back to [ErrorTypeUnknown].
+// A typed nil [*triple_protocol.Error] (or one hidden behind wrapping) also
+// falls back to [ErrorTypeUnknown], which is handled by [triple_protocol.CodeOf].
 //
 // [ErrorTypeNetworkFailure] (network layer) and [ErrorTypeCodec] (codec layer)
 // are reserved but not yet produced: some transport-layer faults and codec
diff --git a/metrics/rpc/error_classifier_test.go b/metrics/rpc/error_classifier_test.go
index 378991ba0..becf7ac02 100644
--- a/metrics/rpc/error_classifier_test.go
+++ b/metrics/rpc/error_classifier_test.go
@@ -18,7 +18,9 @@
 package rpc
 
 import (
+	"context"
 	"errors"
+	"fmt"
 	"testing"
 )
 
@@ -198,3 +200,134 @@ func TestClassifyError_AllErrorTypesClassification(t *testing.T) {
 		})
 	}
 }
+
+// noUnwrapError is an error type that hides the wrapped error behind its
+// Error() method and does not implement Unwrap, so errors.As cannot see
+// through it.
+type noUnwrapError struct {
+	cause error
+}
+
+func (e *noUnwrapError) Error() string {
+	return fmt.Sprintf("no unwrap: %v", e.cause)
+}
+
+func TestClassifyError_Nil(t *testing.T) {
+	var typedNil *triple_protocol.Error
+	tests := []struct {
+		name string
+		err  error
+	}{
+		{
+			name: "untyped nil",
+			err:  nil,
+		},
+		{
+			name: "typed nil triple error",
+			err:  typedNil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, ErrorTypeUnknown, classifyError(tt.err))
+		})
+	}
+}
+
+func TestClassifyError_Boundary(t *testing.T) {
+	tests := []struct {
+		name string
+		err  error
+		want ErrorType
+	}{
+		{
+			name: "empty standard error",
+			err:  errors.New(""),
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "context deadline exceeded",
+			err:  context.DeadlineExceeded,
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "context canceled",
+			err:  context.Canceled,
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "plain error wrapping a triple error without Unwrap",
+			err:  &noUnwrapError{cause: triple_protocol.NewError(triple_protocol.CodeDeadlineExceeded, errors.New("timeout"))},
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "CodeInternal not mapped to a dedicated type yet",
+			err:  triple_protocol.NewError(triple_protocol.CodeInternal, errors.New("internal")),
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "CodeDataLoss",
+			err:  triple_protocol.NewError(triple_protocol.CodeDataLoss, errors.New("data loss")),
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "CodeUnauthenticated",
+			err:  triple_protocol.NewError(triple_protocol.CodeUnauthenticated, errors.New("unauthenticated")),
+			want: ErrorTypeUnknown,
+		},
+		{
+			name: "empty message triple error is classified by its code",
+			err:  triple_protocol.NewError(triple_protocol.CodeUnavailable, nil),
+			want: ErrorTypeServiceUnavailable,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, classifyError(tt.err))
+		})
+	}
+}
+
+func TestClassifyError_Wrapped(t *testing.T) {
+	tests := []struct {
+		name string
+		err  error
+		want ErrorType
+	}{
+		{
+			name: "single level wrap of timeout",
+			err:  fmt.Errorf("rpc failed: %w", triple_protocol.NewError(triple_protocol.CodeDeadlineExceeded, errors.New("timeout"))),
+			want: ErrorTypeTimeout,
+		},
+		{
+			name: "single level wrap of limit",
+			err:  fmt.Errorf("rpc failed: %w", triple_protocol.NewError(triple_protocol.CodeResourceExhausted, errors.New("limit"))),
+			want: ErrorTypeLimit,
+		},
+		{
+			name: "single level wrap of service unavailable",
+			err:  fmt.Errorf("rpc failed: %w", triple_protocol.NewError(triple_protocol.CodeUnavailable, errors.New("unavailable"))),
+			want: ErrorTypeServiceUnavailable,
+		},
+		{
+			name: "single level wrap of business failed",
+			err:  fmt.Errorf("rpc failed: %w", triple_protocol.NewError(triple_protocol.CodeBizError, errors.New("biz error"))),
+			want: ErrorTypeBusinessFailed,
+		},
+		{
+			name: "multi level wrap",
+			err:  fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", triple_protocol.NewError(triple_protocol.CodeDeadlineExceeded, errors.New("timeout")))),
+			want: ErrorTypeTimeout,
+		},
+		{
+			name: "wrap of plain error stays unknown",
+			err:  fmt.Errorf("rpc failed: %w", errors.New("plain error")),
+			want: ErrorTypeUnknown,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, classifyError(tt.err))
+		})
+	}
+}
diff --git a/protocol/triple/triple_protocol/code.go b/protocol/triple/triple_protocol/code.go
index 8bd65bc9d..b854dfde7 100644
--- a/protocol/triple/triple_protocol/code.go
+++ b/protocol/triple/triple_protocol/code.go
@@ -223,9 +223,11 @@ func (c *Code) UnmarshalText(data []byte) error {
 }
 
 // CodeOf returns the error's status code if it is or wraps an [*Error] and
-// [CodeUnknown] otherwise.
+// [CodeUnknown] otherwise. A typed nil [*Error] (e.g. one assigned to an
+// [error] interface and returned as nil) is not caught by err == nil and
+// also yields [CodeUnknown] instead of panicking.
 func CodeOf(err error) Code {
-	if tripleErr, ok := asError(err); ok {
+	if tripleErr, ok := asError(err); ok && tripleErr != nil {
 		return tripleErr.Code()
 	}
 	return CodeUnknown
diff --git a/protocol/triple/triple_protocol/error_test.go b/protocol/triple/triple_protocol/error_test.go
index 7b2615d67..1de35f6ad 100644
--- a/protocol/triple/triple_protocol/error_test.go
+++ b/protocol/triple/triple_protocol/error_test.go
@@ -86,6 +86,11 @@ func TestCodeOf(t *testing.T) {
 		CodeUnavailable,
 	)
 	assert.Equal(t, CodeOf(errors.New("foo")), CodeUnknown)
+	// a typed nil *Error assigned to the error interface escapes err == nil,
+	// but must not panic and must fall back to CodeUnknown
+	var typedNil *Error
+	assert.Equal(t, CodeOf(typedNil), CodeUnknown)
+	assert.Equal(t, CodeOf(fmt.Errorf("wrapped: %w", typedNil)), CodeUnknown)
 }
 
 func TestErrorDetails(t *testing.T) {