Skip to content

Commit e7770c9

Browse files
authored
feat(probe): add Kubernetes probe support with liveness, readiness, and startup checks (#3213)
* feat(probe): add Kubernetes probe support with liveness, readiness, and startup checks
1 parent 3929ee4 commit e7770c9

File tree

15 files changed

+760
-1
lines changed

15 files changed

+760
-1
lines changed

common/constant/key.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,13 @@ const (
476476
PrometheusPushgatewayPasswordKey = "prometheus.pushgateway.password"
477477
PrometheusPushgatewayPushIntervalKey = "prometheus.pushgateway.push.interval"
478478
PrometheusPushgatewayJobKey = "prometheus.pushgateway.job"
479+
480+
ProbeEnabledKey = "probe.enabled"
481+
ProbePortKey = "probe.port"
482+
ProbeLivenessPathKey = "probe.liveness.path"
483+
ProbeReadinessPathKey = "probe.readiness.path"
484+
ProbeStartupPathKey = "probe.startup.path"
485+
ProbeUseInternalStateKey = "probe.use-internal-state"
479486
)
480487

481488
// default meta cache config

common/constant/metric.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
package constant
1919

20+
import (
21+
"time"
22+
)
23+
2024
// metrics type
2125
const (
2226
MetricsRegistry = "dubbo.metrics.registry"
@@ -53,6 +57,13 @@ const (
5357
PrometheusDefaultMetricsPort = "9090"
5458
PrometheusDefaultPushInterval = 30
5559
PrometheusDefaultJobName = "default_dubbo_job"
60+
ProbeDefaultPort = "22222"
61+
ProbeDefaultLivenessPath = "/live"
62+
ProbeDefaultReadinessPath = "/ready"
63+
ProbeDefaultStartupPath = "/startup"
64+
ProbeReadHeaderTimeout = 5 * time.Second
65+
ProbeWriteTimeout = 10 * time.Second
66+
ProbeIdleTimeout = 30 * time.Second
5667
MetricFilterStartTime = "metric_filter_start_time"
5768
)
5869

compat.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,21 @@ func compatMetricConfig(c *global.MetricsConfig) *config.MetricsConfig {
379379
EnableMetadata: c.EnableMetadata,
380380
EnableRegistry: c.EnableRegistry,
381381
EnableConfigCenter: c.EnableConfigCenter,
382+
Probe: compatMetricProbeConfig(c.Probe),
383+
}
384+
}
385+
386+
func compatMetricProbeConfig(c *global.ProbeConfig) *config.ProbeConfig {
387+
if c == nil {
388+
return nil
389+
}
390+
return &config.ProbeConfig{
391+
Enabled: c.Enabled,
392+
Port: c.Port,
393+
LivenessPath: c.LivenessPath,
394+
ReadinessPath: c.ReadinessPath,
395+
StartupPath: c.StartupPath,
396+
UseInternalState: c.UseInternalState,
382397
}
383398
}
384399

@@ -916,6 +931,21 @@ func compatGlobalMetricConfig(c *config.MetricsConfig) *global.MetricsConfig {
916931
EnableMetadata: c.EnableMetadata,
917932
EnableRegistry: c.EnableRegistry,
918933
EnableConfigCenter: c.EnableConfigCenter,
934+
Probe: compatGlobalMetricProbeConfig(c.Probe),
935+
}
936+
}
937+
938+
func compatGlobalMetricProbeConfig(c *config.ProbeConfig) *global.ProbeConfig {
939+
if c == nil {
940+
return nil
941+
}
942+
return &global.ProbeConfig{
943+
Enabled: c.Enabled,
944+
Port: c.Port,
945+
LivenessPath: c.LivenessPath,
946+
ReadinessPath: c.ReadinessPath,
947+
StartupPath: c.StartupPath,
948+
UseInternalState: c.UseInternalState,
919949
}
920950
}
921951

config/metric_config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type MetricsConfig struct {
4444
EnableConfigCenter *bool `default:"false" yaml:"enable-config-center" json:"enable-config-center,omitempty" property:"enable-config-center"`
4545
Prometheus *PrometheusConfig `yaml:"prometheus" json:"prometheus" property:"prometheus"`
4646
Aggregation *AggregateConfig `yaml:"aggregation" json:"aggregation" property:"aggregation"`
47+
Probe *ProbeConfig `yaml:"probe" json:"probe" property:"probe"`
4748
rootConfig *RootConfig
4849
}
4950

@@ -58,6 +59,15 @@ type PrometheusConfig struct {
5859
Pushgateway *PushgatewayConfig `yaml:"pushgateway" json:"pushgateway,omitempty" property:"pushgateway"`
5960
}
6061

62+
type ProbeConfig struct {
63+
Enabled *bool `default:"false" yaml:"enabled" json:"enabled,omitempty" property:"enabled"`
64+
Port string `default:"22222" yaml:"port" json:"port,omitempty" property:"port"`
65+
LivenessPath string `default:"/live" yaml:"liveness-path" json:"liveness-path,omitempty" property:"liveness-path"`
66+
ReadinessPath string `default:"/ready" yaml:"readiness-path" json:"readiness-path,omitempty" property:"readiness-path"`
67+
StartupPath string `default:"/startup" yaml:"startup-path" json:"startup-path,omitempty" property:"startup-path"`
68+
UseInternalState *bool `default:"true" yaml:"use-internal-state" json:"use-internal-state,omitempty" property:"use-internal-state"`
69+
}
70+
6171
type Exporter struct {
6272
Enabled *bool `default:"true" yaml:"enabled" json:"enabled,omitempty" property:"enabled"`
6373
}

global/metric_config.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type MetricsConfig struct {
2828
EnableMetadata *bool `default:"true" yaml:"enable-metadata" json:"enable-metadata,omitempty" property:"enable-metadata"`
2929
EnableRegistry *bool `default:"true" yaml:"enable-registry" json:"enable-registry,omitempty" property:"enable-registry"`
3030
EnableConfigCenter *bool `default:"true" yaml:"enable-config-center" json:"enable-config-center,omitempty" property:"enable-config-center"`
31+
Probe *ProbeConfig `yaml:"probe" json:"probe" property:"probe"`
3132
}
3233

3334
type AggregateConfig struct {
@@ -41,6 +42,15 @@ type PrometheusConfig struct {
4142
Pushgateway *PushgatewayConfig `yaml:"pushgateway" json:"pushgateway,omitempty" property:"pushgateway"`
4243
}
4344

45+
type ProbeConfig struct {
46+
Enabled *bool `default:"false" yaml:"enabled" json:"enabled,omitempty" property:"enabled"`
47+
Port string `default:"22222" yaml:"port" json:"port,omitempty" property:"port"`
48+
LivenessPath string `default:"/live" yaml:"liveness-path" json:"liveness-path,omitempty" property:"liveness-path"`
49+
ReadinessPath string `default:"/ready" yaml:"readiness-path" json:"readiness-path,omitempty" property:"readiness-path"`
50+
StartupPath string `default:"/startup" yaml:"startup-path" json:"startup-path,omitempty" property:"startup-path"`
51+
UseInternalState *bool `default:"true" yaml:"use-internal-state" json:"use-internal-state,omitempty" property:"use-internal-state"`
52+
}
53+
4454
type Exporter struct {
4555
Enabled *bool `default:"false" yaml:"enabled" json:"enabled,omitempty" property:"enabled"`
4656
}
@@ -57,7 +67,7 @@ type PushgatewayConfig struct {
5767

5868
func DefaultMetricsConfig() *MetricsConfig {
5969
// return a new config without setting any field means there is not any default value for initialization
60-
return &MetricsConfig{Prometheus: defaultPrometheusConfig(), Aggregation: defaultAggregateConfig()}
70+
return &MetricsConfig{Prometheus: defaultPrometheusConfig(), Aggregation: defaultAggregateConfig(), Probe: defaultProbeConfig()}
6171
}
6272

6373
// Clone a new MetricsConfig
@@ -100,6 +110,7 @@ func (c *MetricsConfig) Clone() *MetricsConfig {
100110
EnableMetadata: newEnableMetadata,
101111
EnableRegistry: newEnableRegistry,
102112
EnableConfigCenter: newEnableConfigCenter,
113+
Probe: c.Probe.Clone(),
103114
}
104115
}
105116

@@ -155,6 +166,36 @@ func (c *PushgatewayConfig) Clone() *PushgatewayConfig {
155166
}
156167
}
157168

169+
func defaultProbeConfig() *ProbeConfig {
170+
return &ProbeConfig{}
171+
}
172+
173+
func (c *ProbeConfig) Clone() *ProbeConfig {
174+
if c == nil {
175+
return nil
176+
}
177+
178+
var newEnabled *bool
179+
if c.Enabled != nil {
180+
newEnabled = new(bool)
181+
*newEnabled = *c.Enabled
182+
}
183+
var newUseInternalState *bool
184+
if c.UseInternalState != nil {
185+
newUseInternalState = new(bool)
186+
*newUseInternalState = *c.UseInternalState
187+
}
188+
189+
return &ProbeConfig{
190+
Enabled: newEnabled,
191+
Port: c.Port,
192+
LivenessPath: c.LivenessPath,
193+
ReadinessPath: c.ReadinessPath,
194+
StartupPath: c.StartupPath,
195+
UseInternalState: newUseInternalState,
196+
}
197+
}
198+
158199
func defaultAggregateConfig() *AggregateConfig {
159200
return &AggregateConfig{}
160201
}

metrics/options.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,41 @@ func WithPath(path string) Option {
152152
opts.Metrics.Path = path
153153
}
154154
}
155+
156+
// Below are options for probe
157+
func WithProbeEnabled() Option {
158+
return func(opts *Options) {
159+
b := true
160+
opts.Metrics.Probe.Enabled = &b
161+
}
162+
}
163+
164+
func WithProbePort(port int) Option {
165+
return func(opts *Options) {
166+
opts.Metrics.Probe.Port = strconv.Itoa(port)
167+
}
168+
}
169+
170+
func WithProbeLivenessPath(path string) Option {
171+
return func(opts *Options) {
172+
opts.Metrics.Probe.LivenessPath = path
173+
}
174+
}
175+
176+
func WithProbeReadinessPath(path string) Option {
177+
return func(opts *Options) {
178+
opts.Metrics.Probe.ReadinessPath = path
179+
}
180+
}
181+
182+
func WithProbeStartupPath(path string) Option {
183+
return func(opts *Options) {
184+
opts.Metrics.Probe.StartupPath = path
185+
}
186+
}
187+
188+
func WithProbeUseInternalState(use bool) Option {
189+
return func(opts *Options) {
190+
opts.Metrics.Probe.UseInternalState = &use
191+
}
192+
}

metrics/probe/http.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package probe
19+
20+
import (
21+
"errors"
22+
"net/http"
23+
)
24+
25+
var (
26+
errNotReady = errors.New("not ready")
27+
errNotStarted = errors.New("not started")
28+
)
29+
30+
func livenessHandler(w http.ResponseWriter, r *http.Request) {
31+
// k8s only use GET
32+
if r.Method != http.MethodGet {
33+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
34+
return
35+
}
36+
37+
if err := CheckLiveness(r.Context()); err != nil {
38+
http.Error(w, err.Error(), http.StatusServiceUnavailable)
39+
return
40+
}
41+
w.WriteHeader(http.StatusOK)
42+
_, _ = w.Write([]byte("ok"))
43+
}
44+
45+
func readinessHandler(w http.ResponseWriter, r *http.Request) {
46+
// k8s only use GET
47+
if r.Method != http.MethodGet {
48+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
49+
return
50+
}
51+
52+
if err := CheckReadiness(r.Context()); err != nil {
53+
http.Error(w, err.Error(), http.StatusServiceUnavailable)
54+
return
55+
}
56+
w.WriteHeader(http.StatusOK)
57+
_, _ = w.Write([]byte("ok"))
58+
}
59+
60+
func startupHandler(w http.ResponseWriter, r *http.Request) {
61+
// k8s only use GET
62+
if r.Method != http.MethodGet {
63+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
64+
return
65+
}
66+
67+
if err := CheckStartup(r.Context()); err != nil {
68+
http.Error(w, err.Error(), http.StatusServiceUnavailable)
69+
return
70+
}
71+
w.WriteHeader(http.StatusOK)
72+
_, _ = w.Write([]byte("ok"))
73+
}

metrics/probe/http_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package probe
19+
20+
import (
21+
"context"
22+
"errors"
23+
"net/http"
24+
"net/http/httptest"
25+
"testing"
26+
)
27+
28+
func TestHandlersSuccess(t *testing.T) {
29+
resetProbeState()
30+
31+
RegisterLiveness("ok", func(context.Context) error { return nil })
32+
RegisterReadiness("ok", func(context.Context) error { return nil })
33+
RegisterStartup("ok", func(context.Context) error { return nil })
34+
35+
req := httptest.NewRequest(http.MethodGet, "/live", nil)
36+
rec := httptest.NewRecorder()
37+
livenessHandler(rec, req)
38+
if rec.Code != http.StatusOK {
39+
t.Fatalf("expected 200 for liveness, got %d", rec.Code)
40+
}
41+
42+
req = httptest.NewRequest(http.MethodGet, "/ready", nil)
43+
rec = httptest.NewRecorder()
44+
readinessHandler(rec, req)
45+
if rec.Code != http.StatusOK {
46+
t.Fatalf("expected 200 for readiness, got %d", rec.Code)
47+
}
48+
49+
req = httptest.NewRequest(http.MethodGet, "/startup", nil)
50+
rec = httptest.NewRecorder()
51+
startupHandler(rec, req)
52+
if rec.Code != http.StatusOK {
53+
t.Fatalf("expected 200 for startup, got %d", rec.Code)
54+
}
55+
}
56+
57+
func TestHandlersFailure(t *testing.T) {
58+
resetProbeState()
59+
60+
RegisterLiveness("fail", func(context.Context) error { return errors.New("bad") })
61+
RegisterReadiness("fail", func(context.Context) error { return errors.New("bad") })
62+
RegisterStartup("fail", func(context.Context) error { return errors.New("bad") })
63+
64+
req := httptest.NewRequest(http.MethodGet, "/live", nil)
65+
rec := httptest.NewRecorder()
66+
livenessHandler(rec, req)
67+
if rec.Code != http.StatusServiceUnavailable {
68+
t.Fatalf("expected 503 for liveness, got %d", rec.Code)
69+
}
70+
71+
req = httptest.NewRequest(http.MethodGet, "/ready", nil)
72+
rec = httptest.NewRecorder()
73+
readinessHandler(rec, req)
74+
if rec.Code != http.StatusServiceUnavailable {
75+
t.Fatalf("expected 503 for readiness, got %d", rec.Code)
76+
}
77+
78+
req = httptest.NewRequest(http.MethodGet, "/startup", nil)
79+
rec = httptest.NewRecorder()
80+
startupHandler(rec, req)
81+
if rec.Code != http.StatusServiceUnavailable {
82+
t.Fatalf("expected 503 for startup, got %d", rec.Code)
83+
}
84+
}

0 commit comments

Comments
 (0)