implemented support for custom timeout parameter for ping/reachability analyzer
This commit is contained in:
47
README.md
47
README.md
@@ -36,7 +36,16 @@ yosoy responds to all requests with a JSON containing the information about:
|
|||||||
|
|
||||||
Check [Sample JSON response](#sample-json-response) to see how you can use yosoy for stubbing/prototyping/troubleshooting distributed applications.
|
Check [Sample JSON response](#sample-json-response) to see how you can use yosoy for stubbing/prototyping/troubleshooting distributed applications.
|
||||||
|
|
||||||
Check [ping/reachability analyzer](#pingreachability-analyzer) to see how you can use yosoy for troubleshooting network connectivity.
|
## ping/reachability analyzer
|
||||||
|
|
||||||
|
yosoy includes a simple ping/reachability analyzer. You can use this functionality when prototyping distributed systems to validate whether a given component can reach a specific endpoint. yosoy exposes a dedicated `/_/yosoy/ping` endpoint which accepts the following 4 query parameters:
|
||||||
|
|
||||||
|
* `h` - required - hostname of the endpoint
|
||||||
|
* `p` - required - port of the endpoint
|
||||||
|
* `n` - optional - network, all valid Go networks are supported (including the most popular ones like `tcp`, `udp`, IPv4, IPV6, etc.). If `n` parameter is not provided, it defaults to `tcp`. If `n` parameter is set to unknown network, an error will be returned.
|
||||||
|
* `t` - optional - timeout in seconds. If `t` parameter is not provided, it defaults to `10`. If `t` contains invalid integer literal, an error will be returned.
|
||||||
|
|
||||||
|
Check [Sample ping/reachability analyzer responses](#sample-pingreachability-analyzer-responses) to see how you can use yosoy for troubleshooting network connectivity.
|
||||||
|
|
||||||
## Docker image
|
## Docker image
|
||||||
|
|
||||||
@@ -132,24 +141,40 @@ A sample yosoy JSON response to a request made from a single page application (S
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## ping/reachability analyzer
|
## Sample ping/reachability analyzer responses
|
||||||
|
|
||||||
yosoy includes a simple ping/reachability analyzer. You can use this functionality when prototyping distributed systems to validate whether a given component can reach a specific endpoint. yosoy exposes a dedicated `/_/yosoy/ping` endpoint which accepts the following 3 query parameters:
|
To test if yosoy can connect to `google.com` on port `443` using default `tcp` network use the following command:
|
||||||
|
|
||||||
* `h` - required - hostname of the endpoint
|
|
||||||
* `p` - required - port of the endpoint
|
|
||||||
* `n` - optional - network, all valid Go networks are supported (including the most popular ones like `tcp`, `udp`, IPv4, IPV6, etc.). If `n` parameter is not provided, it defaults to `tcp`. Go will throw an error if `n` parameter will be set to unknown network.
|
|
||||||
|
|
||||||
For example, to test if yosoy can connect to `google.com` on port `443` using default `tcp` network use the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl "$URL/_/yosoy/ping?h=google.com&p=443"
|
curl -v "http://localhost/_/yosoy/ping?h=google.com&p=443"
|
||||||
|
> GET /_/yosoy/ping?h=google.com&p=443 HTTP/1.1
|
||||||
|
> Host: localhost
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Date: Fri, 17 Nov 2023 05:54:36 GMT
|
||||||
|
< Content-Length: 29
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"message":"ping succeeded"}
|
||||||
```
|
```
|
||||||
|
|
||||||
To see an unsuccessful response you may use localhost with some random port number:
|
To see an unsuccessful response you may use localhost with some random port number:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl "$URL/_/yosoy/ping?h=127.0.0.1&p=12345"
|
curl -v "http://localhost/_/yosoy/ping?h=127.0.0.1&p=12345"
|
||||||
|
> GET /_/yosoy/ping?h=127.0.0.1&p=12345 HTTP/1.1
|
||||||
|
> Host: localhost
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 500 Internal Server Error
|
||||||
|
< Date: Fri, 17 Nov 2023 05:53:48 GMT
|
||||||
|
< Content-Length: 66
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"error":"dial tcp 127.0.0.1:12345: connect: connection refused"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building and testing locally
|
## Building and testing locally
|
||||||
|
|||||||
35
server.go
35
server.go
@@ -6,12 +6,17 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PING_DEFAULT_TIMEOUT = 10
|
||||||
|
const PING_DEFAULT_NETWORK = "tcp"
|
||||||
|
|
||||||
type response struct {
|
type response struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Proto string `json:"proto"`
|
Proto string `json:"proto"`
|
||||||
@@ -95,10 +100,12 @@ func handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ping(w http.ResponseWriter, req *http.Request) {
|
func ping(w http.ResponseWriter, req *http.Request) {
|
||||||
// get h, p, t parameters from query string
|
// get h, p, n, t parameters from query string
|
||||||
hostname := req.URL.Query().Get("h")
|
hostname := req.URL.Query().Get("h")
|
||||||
port := req.URL.Query().Get("p")
|
port := req.URL.Query().Get("p")
|
||||||
network := req.URL.Query().Get("n")
|
network := req.URL.Query().Get("n")
|
||||||
|
timeoutString := req.URL.Query().Get("t")
|
||||||
|
var timeout int64 = PING_DEFAULT_TIMEOUT
|
||||||
|
|
||||||
// return HTTP BadRequest when hostname is empty
|
// return HTTP BadRequest when hostname is empty
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
@@ -114,12 +121,25 @@ func ping(w http.ResponseWriter, req *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(&errorResponse{"port is empty"})
|
json.NewEncoder(w).Encode(&errorResponse{"port is empty"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// check if timeoutString is a valid int64, return HTTP BadRequest when invalid, otherwise set timeout value
|
||||||
|
if timeoutString != "" {
|
||||||
|
timeoutInt, err := strconv.ParseInt(timeoutString, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{"timeout is invalid"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeout = timeoutInt
|
||||||
|
}
|
||||||
|
|
||||||
// if network is empty set default to tcp
|
// if network is empty set default to tcp
|
||||||
if network == "" {
|
if network == "" {
|
||||||
network = "tcp"
|
network = PING_DEFAULT_NETWORK
|
||||||
}
|
}
|
||||||
// ping the hostname and port by opening a socket
|
|
||||||
err := pingHost(hostname, port, network)
|
// ping the hostname and port
|
||||||
|
err := pingHost(hostname, port, network, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -132,9 +152,12 @@ func ping(w http.ResponseWriter, req *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(&successResponse{"ping succeeded"})
|
json.NewEncoder(w).Encode(&successResponse{"ping succeeded"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func pingHost(hostname, port, network string) error {
|
func pingHost(hostname, port, network string, timeout int64) error {
|
||||||
|
// create timeoutDuration variable of Duration type using timeout as the value in seconds
|
||||||
|
timeoutDuration := time.Duration(timeout) * time.Second
|
||||||
|
|
||||||
// open a socket to hostname and port
|
// open a socket to hostname and port
|
||||||
conn, err := net.Dial(network, hostname+":"+port)
|
conn, err := net.DialTimeout(network, hostname+":"+port, timeoutDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,3 +235,71 @@ func TestHandlerPreflight(t *testing.T) {
|
|||||||
assert.Equal(t, "600", result.Header.Get("Access-Control-Max-Age"))
|
assert.Equal(t, "600", result.Header.Get("Access-Control-Max-Age"))
|
||||||
assert.Equal(t, "*", result.Header.Get("Access-Control-Expose-Headers"))
|
assert.Equal(t, "*", result.Header.Get("Access-Control-Expose-Headers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5, the request should return 200 ok and return JSON with message "ping succeeded"
|
||||||
|
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeout(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
var response successResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "ping succeeded", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid, the request should return 400 bad request and return JSON with error "timeout is invalid"
|
||||||
|
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeoutInvalid(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusBadRequest {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "timeout is invalid", response.Error)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user