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 <[email protected]>
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) {

Reply via email to