Add support for grpc health check (#835)
authorAlessandro Verzicco <8068317+averzicco@users.noreply.github.com>
Mon, 15 Nov 2021 08:41:34 +0000 (09:41 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Nov 2021 08:41:34 +0000 (09:41 +0100)
Signed-off-by: Alessandro Verzicco <8068317+averzicco@users.noreply.github.com>
blackbox.yml
config/config.go
go.mod
go.sum
main.go
prober/grpc.go [new file with mode: 0644]
prober/grpc_test.go [new file with mode: 0644]

index 65c97cc0c67246e2c40db72dcd6f9c4b6f5c9a05..8418399ea451c9ba189b437469be3a59ff8373b0 100644 (file)
@@ -15,6 +15,16 @@ modules:
       tls: true
       tls_config:
         insecure_skip_verify: false
+  grpc:
+    prober: grpc
+    grpc:
+      tls: true
+      preferred_ip_protocol: "ip4"
+  grpc_plain:
+    prober: grpc
+    grpc:
+      tls: false
+      service: "service1"
   ssh_banner:
     prober: tcp
     tcp:
index 3e246bc6079df8a1a656abe6e33ba5c67b9568b5..e36b47c9f86e45c436924c469b6937a57b13cb8c 100644 (file)
@@ -63,6 +63,12 @@ var (
                HTTPClientConfig:   config.DefaultHTTPClientConfig,
        }
 
+       // DefaultGRPCProbe set default value for HTTPProbe
+       DefaultGRPCProbe = GRPCProbe{
+               Service:            "",
+               IPProtocolFallback: true,
+       }
+
        // DefaultTCPProbe set default value for TCPProbe
        DefaultTCPProbe = TCPProbe{
                IPProtocolFallback: true,
@@ -188,6 +194,7 @@ type Module struct {
        TCP     TCPProbe      `yaml:"tcp,omitempty"`
        ICMP    ICMPProbe     `yaml:"icmp,omitempty"`
        DNS     DNSProbe      `yaml:"dns,omitempty"`
+       GRPC    GRPCProbe     `yaml:"grpc,omitempty"`
 }
 
 type HTTPProbe struct {
@@ -211,6 +218,14 @@ type HTTPProbe struct {
        BodySizeLimit                units.Base2Bytes        `yaml:"body_size_limit,omitempty"`
 }
 
+type GRPCProbe struct {
+       Service             string           `yaml:"service,omitempty"`
+       TLS                 bool             `yaml:"tls,omitempty"`
+       TLSConfig           config.TLSConfig `yaml:"tls_config,omitempty"`
+       IPProtocolFallback  bool             `yaml:"ip_protocol_fallback,omitempty"`
+       PreferredIPProtocol string           `yaml:"preferred_ip_protocol,omitempty"`
+}
+
 type HeaderMatch struct {
        Header       string `yaml:"header,omitempty"`
        Regexp       Regexp `yaml:"regexp,omitempty"`
@@ -320,6 +335,16 @@ func (s *HTTPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
        return nil
 }
 
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (s *GRPCProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
+       *s = DefaultGRPCProbe
+       type plain GRPCProbe
+       if err := unmarshal((*plain)(s)); err != nil {
+               return err
+       }
+       return nil
+}
+
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
 func (s *DNSProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
        *s = DefaultDNSProbe
diff --git a/go.mod b/go.mod
index befc41d3eb373eb2d3c8e8c8eee15c2314818165..37006139fe55e5cf56744e99676bef7769f1d052 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -8,9 +8,10 @@ require (
        github.com/pkg/errors v0.9.1
        github.com/prometheus/client_golang v1.11.0
        github.com/prometheus/client_model v0.2.0
-       github.com/prometheus/common v0.31.1
+       github.com/prometheus/common v0.32.1
        github.com/prometheus/exporter-toolkit v0.7.0
-       golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
+       golang.org/x/net v0.0.0-20211020060615-d418f374d309
+       google.golang.org/grpc v1.41.0
        gopkg.in/alecthomas/kingpin.v2 v2.2.6
        gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )
diff --git a/go.sum b/go.sum
index e3defb0c558badca5a3fdd55b45e015073e445c9..03d1989f762cc855c52f42113e4948757089e8bd 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -43,6 +43,7 @@ github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5K
 github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
 github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -55,13 +56,18 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -126,8 +132,10 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -180,8 +188,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.31.1 h1:d18hG4PkHnNAKNMOmFuXFaiY8Us0nird/2m60uS1AMs=
-github.com/prometheus/common v0.31.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
 github.com/prometheus/exporter-toolkit v0.7.0 h1:XtYeVeeC5daG4txbc9+mieKq+/AK4gtIBLl9Mulrjnk=
 github.com/prometheus/exporter-toolkit v0.7.0/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -189,6 +197,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -197,8 +206,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -207,6 +218,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -275,8 +287,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -435,11 +447,13 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -453,6 +467,10 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
+google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -474,11 +492,13 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/main.go b/main.go
index 689ab8035fbdfd0564c5a6f4395860333ef2e50a..0c5a14f428479ad65e1478b3e98d9b46c6410658 100644 (file)
--- a/main.go
+++ b/main.go
@@ -67,6 +67,7 @@ var (
                "tcp":  prober.ProbeTCP,
                "icmp": prober.ProbeICMP,
                "dns":  prober.ProbeDNS,
+               "grpc": prober.ProbeGRPC,
        }
 
        moduleUnknownCounter = prometheus.NewCounter(prometheus.CounterOpts{
diff --git a/prober/grpc.go b/prober/grpc.go
new file mode 100644 (file)
index 0000000..c528597
--- /dev/null
@@ -0,0 +1,219 @@
+// Copyright 2021 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package prober
+
+import (
+       "context"
+       "github.com/go-kit/log"
+       "github.com/go-kit/log/level"
+       "github.com/prometheus/blackbox_exporter/config"
+       "github.com/prometheus/client_golang/prometheus"
+       pconfig "github.com/prometheus/common/config"
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/codes"
+       "google.golang.org/grpc/credentials"
+       "google.golang.org/grpc/health/grpc_health_v1"
+       "google.golang.org/grpc/peer"
+       "google.golang.org/grpc/status"
+       "net"
+       "net/url"
+       "strings"
+       "time"
+)
+
+type GRPCHealthCheck interface {
+       Check(c context.Context, service string) (bool, codes.Code, *peer.Peer, string, error)
+}
+
+type gRPCHealthCheckClient struct {
+       client grpc_health_v1.HealthClient
+       conn   *grpc.ClientConn
+}
+
+func NewGrpcHealthCheckClient(conn *grpc.ClientConn) GRPCHealthCheck {
+       client := new(gRPCHealthCheckClient)
+       client.client = grpc_health_v1.NewHealthClient(conn)
+       client.conn = conn
+       return client
+}
+
+func (c *gRPCHealthCheckClient) Close() error {
+       return c.conn.Close()
+}
+
+func (c *gRPCHealthCheckClient) Check(ctx context.Context, service string) (bool, codes.Code, *peer.Peer, string, error) {
+       var res *grpc_health_v1.HealthCheckResponse
+       var err error
+       req := grpc_health_v1.HealthCheckRequest{
+               Service: service,
+       }
+
+       serverPeer := new(peer.Peer)
+       res, err = c.client.Check(ctx, &req, grpc.Peer(serverPeer))
+       if err == nil {
+               if res.GetStatus() == grpc_health_v1.HealthCheckResponse_SERVING {
+                       return true, codes.OK, serverPeer, res.Status.String(), nil
+               }
+               return false, codes.OK, serverPeer, res.Status.String(), nil
+       }
+
+       returnStatus, _ := status.FromError(err)
+
+       return false, returnStatus.Code(), nil, "", err
+}
+
+func ProbeGRPC(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) {
+
+       var (
+               durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+                       Name: "probe_grpc_duration_seconds",
+                       Help: "Duration of gRPC request by phase",
+               }, []string{"phase"})
+
+               isSSLGauge = prometheus.NewGauge(prometheus.GaugeOpts{
+                       Name: "probe_grpc_ssl",
+                       Help: "Indicates if SSL was used for the connection",
+               })
+
+               statusCodeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
+                       Name: "probe_grpc_status_code",
+                       Help: "Response gRPC status code",
+               })
+
+               healthCheckResponseGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+                       Name: "probe_grpc_healthcheck_response",
+                       Help: "Response HealthCheck response",
+               }, []string{"serving_status"})
+
+               probeSSLEarliestCertExpiryGauge = prometheus.NewGauge(prometheus.GaugeOpts{
+                       Name: "probe_ssl_earliest_cert_expiry",
+                       Help: "Returns earliest SSL cert expiry in unixtime",
+               })
+
+               probeTLSVersion = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+                       Name: "probe_tls_version_info",
+                       Help: "Contains the TLS version used",
+               },
+                       []string{"version"},
+               )
+       )
+
+       for _, lv := range []string{"resolve"} {
+               durationGaugeVec.WithLabelValues(lv)
+       }
+
+       registry.MustRegister(durationGaugeVec)
+       registry.MustRegister(isSSLGauge)
+       registry.MustRegister(statusCodeGauge)
+       registry.MustRegister(healthCheckResponseGaugeVec)
+       registry.MustRegister(probeSSLEarliestCertExpiryGauge)
+       registry.MustRegister(probeTLSVersion)
+
+       if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
+               target = "http://" + target
+       }
+
+       targetURL, err := url.Parse(target)
+       if err != nil {
+               level.Error(logger).Log("msg", "Could not parse target URL", "err", err)
+               return false
+       }
+
+       targetHost, targetPort, err := net.SplitHostPort(targetURL.Host)
+       // If split fails, assuming it's a hostname without port part.
+       if err != nil {
+               targetHost = targetURL.Host
+       }
+
+       tlsConfig, err := pconfig.NewTLSConfig(&module.GRPC.TLSConfig)
+       if err != nil {
+               level.Error(logger).Log("msg", "Error creating TLS configuration", "err", err)
+               return false
+       }
+
+       ip, lookupTime, err := chooseProtocol(ctx, module.GRPC.PreferredIPProtocol, module.GRPC.IPProtocolFallback, targetHost, registry, logger)
+       if err != nil {
+               level.Error(logger).Log("msg", "Error resolving address", "err", err)
+               return false
+       }
+       durationGaugeVec.WithLabelValues("resolve").Add(lookupTime)
+       checkStart := time.Now()
+       if len(tlsConfig.ServerName) == 0 {
+               // If there is no `server_name` in tls_config, use
+               // the hostname of the target.
+               tlsConfig.ServerName = targetHost
+       }
+
+       if targetPort == "" {
+               targetURL.Host = "[" + ip.String() + "]"
+       } else {
+               targetURL.Host = net.JoinHostPort(ip.String(), targetPort)
+       }
+
+       var opts []grpc.DialOption
+       target = targetHost + ":" + targetPort
+       if !module.GRPC.TLS {
+               level.Debug(logger).Log("msg", "Dialing GRPC without TLS")
+               opts = append(opts, grpc.WithInsecure())
+               if len(targetPort) == 0 {
+                       target = targetHost + ":80"
+               }
+       } else {
+               creds := credentials.NewTLS(tlsConfig)
+               opts = append(opts, grpc.WithTransportCredentials(creds))
+               if len(targetPort) == 0 {
+                       target = targetHost + ":443"
+               }
+       }
+
+       conn, err := grpc.Dial(target, opts...)
+
+       if err != nil {
+               level.Error(logger).Log("did not connect: %v", err)
+       }
+
+       client := NewGrpcHealthCheckClient(conn)
+       defer conn.Close()
+       ok, statusCode, serverPeer, servingStatus, err := client.Check(context.Background(), module.GRPC.Service)
+       durationGaugeVec.WithLabelValues("check").Add(time.Since(checkStart).Seconds())
+
+       for servingStatusName, _ := range grpc_health_v1.HealthCheckResponse_ServingStatus_value {
+               healthCheckResponseGaugeVec.WithLabelValues(servingStatusName).Set(float64(0))
+       }
+       if servingStatus != "" {
+               healthCheckResponseGaugeVec.WithLabelValues(servingStatus).Set(float64(1))
+       }
+
+       if serverPeer != nil {
+               tlsInfo, tlsOk := serverPeer.AuthInfo.(credentials.TLSInfo)
+               if tlsOk {
+                       isSSLGauge.Set(float64(1))
+                       probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(&tlsInfo.State).Unix()))
+                       probeTLSVersion.WithLabelValues(getTLSVersion(&tlsInfo.State)).Set(1)
+               } else {
+                       isSSLGauge.Set(float64(0))
+               }
+       }
+       statusCodeGauge.Set(float64(statusCode))
+
+       if !ok || err != nil {
+               level.Error(logger).Log("msg", "can't connect grpc server:", "err", err)
+               success = false
+       } else {
+               level.Debug(logger).Log("connect the grpc server successfully")
+               success = true
+       }
+
+       return
+}
diff --git a/prober/grpc_test.go b/prober/grpc_test.go
new file mode 100644 (file)
index 0000000..acb0513
--- /dev/null
@@ -0,0 +1,416 @@
+// Copyright 2021 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package prober
+
+import (
+       "context"
+       "crypto/tls"
+       "crypto/x509"
+       "encoding/pem"
+       "fmt"
+       "github.com/go-kit/log"
+       "github.com/prometheus/blackbox_exporter/config"
+       "github.com/prometheus/client_golang/prometheus"
+       pconfig "github.com/prometheus/common/config"
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/credentials"
+       "google.golang.org/grpc/health"
+       "google.golang.org/grpc/health/grpc_health_v1"
+       "io/ioutil"
+       "net"
+       "os"
+       "testing"
+       "time"
+)
+
+func TestGRPCConnection(t *testing.T) {
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+       s := grpc.NewServer()
+       healthServer := health.NewServer()
+       healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
+       grpc_health_v1.RegisterHealthServer(s, healthServer)
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registry := prometheus.NewRegistry()
+
+       result := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+               },
+               }, registry, log.NewNopLogger())
+
+       if !result {
+               t.Fatalf("GRPC probe failed")
+       }
+
+       mfs, err := registry.Gather()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expectedMetrics := map[string]map[string]map[string]struct{}{
+               "probe_grpc_healthcheck_response": {
+                       "serving_status": {
+                               "UNKNOWN":         {},
+                               "SERVING":         {},
+                               "NOT_SERVING":     {},
+                               "SERVICE_UNKNOWN": {},
+                       },
+               },
+       }
+
+       checkMetrics(expectedMetrics, mfs, t)
+
+       expectedResults := map[string]float64{
+               "probe_grpc_ssl":         0,
+               "probe_grpc_status_code": 0,
+       }
+
+       checkRegistryResults(expectedResults, mfs, t)
+}
+
+func TestMultipleGRPCservices(t *testing.T) {
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+       s := grpc.NewServer()
+       healthServer := health.NewServer()
+       healthServer.SetServingStatus("service1", grpc_health_v1.HealthCheckResponse_SERVING)
+       healthServer.SetServingStatus("service2", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
+       grpc_health_v1.RegisterHealthServer(s, healthServer)
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registryService1 := prometheus.NewRegistry()
+
+       resultService1 := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+                       Service:            "service1",
+               },
+               }, registryService1, log.NewNopLogger())
+
+       if !resultService1 {
+               t.Fatalf("GRPC probe failed for service1")
+       }
+
+       registryService2 := prometheus.NewRegistry()
+       resultService2 := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+                       Service:            "service2",
+               },
+               }, registryService2, log.NewNopLogger())
+
+       if resultService2 {
+               t.Fatalf("GRPC probe succeed for service2")
+       }
+
+       registryService3 := prometheus.NewRegistry()
+       resultService3 := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+                       Service:            "service3",
+               },
+               }, registryService3, log.NewNopLogger())
+
+       if resultService3 {
+               t.Fatalf("GRPC probe succeed for service3")
+       }
+}
+
+func TestGRPCTLSConnection(t *testing.T) {
+
+       certExpiry := time.Now().AddDate(0, 0, 1)
+       testCertTmpl := generateCertificateTemplate(certExpiry, false)
+       testCertTmpl.IsCA = true
+       _, testcertPem, testKey := generateSelfSignedCertificate(testCertTmpl)
+
+       // CAFile must be passed via filesystem, use a tempfile.
+       tmpCaFile, err := ioutil.TempFile("", "cafile.pem")
+       if err != nil {
+               t.Fatalf("Error creating CA tempfile: %s", err)
+       }
+       if _, err = tmpCaFile.Write(testcertPem); err != nil {
+               t.Fatalf("Error writing CA tempfile: %s", err)
+       }
+       if err = tmpCaFile.Close(); err != nil {
+               t.Fatalf("Error closing CA tempfile: %s", err)
+       }
+       defer os.Remove(tmpCaFile.Name())
+
+       testKeyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(testKey)})
+       testcert, err := tls.X509KeyPair(testcertPem, testKeyPem)
+       if err != nil {
+               panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err))
+       }
+
+       tlsConfig := &tls.Config{
+               Certificates: []tls.Certificate{testcert},
+               MinVersion:   tls.VersionTLS12,
+               MaxVersion:   tls.VersionTLS12,
+       }
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+
+       s := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
+       healthServer := health.NewServer()
+       healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
+       grpc_health_v1.RegisterHealthServer(s, healthServer)
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registry := prometheus.NewRegistry()
+
+       result := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       TLS:                true,
+                       TLSConfig:          pconfig.TLSConfig{InsecureSkipVerify: true},
+                       IPProtocolFallback: false,
+               },
+               }, registry, log.NewNopLogger())
+
+       if !result {
+               t.Fatalf("GRPC probe failed")
+       }
+
+       mfs, err := registry.Gather()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expectedLabels := map[string]map[string]string{
+               "probe_tls_version_info": {
+                       "version": "TLS 1.2",
+               },
+       }
+       checkRegistryLabels(expectedLabels, mfs, t)
+
+       expectedResults := map[string]float64{
+               "probe_grpc_ssl":         1,
+               "probe_grpc_status_code": 0,
+       }
+
+       checkRegistryResults(expectedResults, mfs, t)
+}
+
+func TestNoTLSConnection(t *testing.T) {
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+       s := grpc.NewServer()
+       healthServer := health.NewServer()
+       healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
+       grpc_health_v1.RegisterHealthServer(s, healthServer)
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registry := prometheus.NewRegistry()
+
+       result := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       TLS:                true,
+                       TLSConfig:          pconfig.TLSConfig{InsecureSkipVerify: true},
+                       IPProtocolFallback: false,
+               },
+               }, registry, log.NewNopLogger())
+
+       if result {
+               t.Fatalf("GRPC probe succeed")
+       }
+
+       mfs, err := registry.Gather()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expectedResults := map[string]float64{
+               "probe_grpc_ssl":         0,
+               "probe_grpc_status_code": 14, //UNAVAILABLE
+       }
+
+       checkRegistryResults(expectedResults, mfs, t)
+
+}
+
+func TestGRPCServiceNotFound(t *testing.T) {
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+       s := grpc.NewServer()
+       healthServer := health.NewServer()
+       healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
+       grpc_health_v1.RegisterHealthServer(s, healthServer)
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registry := prometheus.NewRegistry()
+
+       result := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+                       Service:            "NonExistingService",
+               },
+               }, registry, log.NewNopLogger())
+
+       if result {
+               t.Fatalf("GRPC probe succeed")
+       }
+
+       mfs, err := registry.Gather()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expectedResults := map[string]float64{
+               "probe_grpc_ssl":         0,
+               "probe_grpc_status_code": 5, //NOT_FOUND
+       }
+
+       checkRegistryResults(expectedResults, mfs, t)
+}
+
+func TestGRPCHealthCheckUnimplemented(t *testing.T) {
+
+       ln, err := net.Listen("tcp", "localhost:0")
+       if err != nil {
+               t.Fatalf("Error listening on socket: %s", err)
+       }
+       defer ln.Close()
+
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatalf("Error retrieving port for socket: %s", err)
+       }
+       s := grpc.NewServer()
+
+       go func() {
+               if err := s.Serve(ln); err != nil {
+                       t.Errorf("failed to serve: %v", err)
+                       return
+               }
+       }()
+       defer s.GracefulStop()
+
+       testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+       registry := prometheus.NewRegistry()
+
+       result := ProbeGRPC(testCTX, "localhost:"+port,
+               config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
+                       IPProtocolFallback: false,
+                       Service:            "NonExistingService",
+               },
+               }, registry, log.NewNopLogger())
+
+       if result {
+               t.Fatalf("GRPC probe succeed")
+       }
+
+       mfs, err := registry.Gather()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       expectedResults := map[string]float64{
+               "probe_grpc_ssl":         0,
+               "probe_grpc_status_code": 12, //UNIMPLEMENTED
+       }
+
+       checkRegistryResults(expectedResults, mfs, t)
+}