feat_: status-backend health endpoint (#6201)
* feat_: status-backend health endpoint * test_: health check * test_: use health endpoint from python
This commit is contained in:
parent
6a5623bac6
commit
309d17ae5b
|
@ -245,6 +245,12 @@ Access the exposed API with any HTTP client you prefer:
|
||||||
- [Python](https://pypi.org/project/requests/)
|
- [Python](https://pypi.org/project/requests/)
|
||||||
- [Go](https://pkg.go.dev/net/http)
|
- [Go](https://pkg.go.dev/net/http)
|
||||||
|
|
||||||
|
## `status-backend` API
|
||||||
|
|
||||||
|
- `/health`
|
||||||
|
This is a basic health check endpoint. Response contains a single `version` property.
|
||||||
|
Returns HTTP code 200 if alive.
|
||||||
|
|
||||||
# 👌 Simple flows
|
# 👌 Simple flows
|
||||||
|
|
||||||
In most cases to start testing you'll need some boilerplate. Below are the simple call flows for common cases.
|
In most cases to start testing you'll need some boilerplate. Below are the simple call flows for common cases.
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthResponse struct {
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Health(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := HealthResponse{
|
||||||
|
Version: version.Version(),
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(response)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/cmd/status-backend/server/api"
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -93,6 +94,7 @@ func (s *Server) Listen(address string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mux = http.NewServeMux()
|
s.mux = http.NewServeMux()
|
||||||
|
s.mux.HandleFunc("/health", api.Health)
|
||||||
s.mux.HandleFunc("/signals", s.signals)
|
s.mux.HandleFunc("/signals", s.signals)
|
||||||
s.server.Handler = s.mux
|
s.server.Handler = s.mux
|
||||||
|
|
||||||
|
|
|
@ -47,4 +47,21 @@ Functional tests for status-go
|
||||||
- Every test has two types of verifications:
|
- Every test has two types of verifications:
|
||||||
- `verify_is_valid_json_rpc_response()` checks for status code 200, non-empty response, JSON-RPC structure, presence of the `result` field, and expected ID.
|
- `verify_is_valid_json_rpc_response()` checks for status code 200, non-empty response, JSON-RPC structure, presence of the `result` field, and expected ID.
|
||||||
- `jsonschema.validate()` is used to check that the response contains expected data, including required fields and types. Schemas are stored in `/schemas/wallet_MethodName`
|
- `jsonschema.validate()` is used to check that the response contains expected data, including required fields and types. Schemas are stored in `/schemas/wallet_MethodName`
|
||||||
- New schemas can be generated using `./tests-functional/utils/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method, should be used only on test creation phase, please search `how to create schema:` to see an example in a test
|
- New schemas can be generated using `./tests-functional/utils/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method, should be used only on test creation phase, please search `how to create schema:` to see an example in a test
|
||||||
|
|
||||||
|
# Known issues
|
||||||
|
|
||||||
|
## Docker permission denied
|
||||||
|
|
||||||
|
When running tests with auto-creating status-backend containers, you might face this:
|
||||||
|
```shell
|
||||||
|
sock.connect(self.unix_socket)
|
||||||
|
PermissionError: [Errno 13] Permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
Please follow this fix: https://github.com/docker/compose/issues/10299#issuecomment-1438247730
|
||||||
|
|
||||||
|
If you're on MacOS and `/var/run/docker.sock` doesn't exist, you need to create a symlink to the docker socket:
|
||||||
|
```shell
|
||||||
|
sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock
|
||||||
|
```
|
|
@ -14,6 +14,8 @@ from datetime import datetime
|
||||||
from conftest import option
|
from conftest import option
|
||||||
from resources.constants import user_1, DEFAULT_DISPLAY_NAME, USER_DIR
|
from resources.constants import user_1, DEFAULT_DISPLAY_NAME, USER_DIR
|
||||||
|
|
||||||
|
NANOSECONDS_PER_SECOND = 1_000_000_000
|
||||||
|
|
||||||
|
|
||||||
class StatusBackend(RpcClient, SignalClient):
|
class StatusBackend(RpcClient, SignalClient):
|
||||||
|
|
||||||
|
@ -29,6 +31,7 @@ class StatusBackend(RpcClient, SignalClient):
|
||||||
url = f"http://127.0.0.1:{host_port}"
|
url = f"http://127.0.0.1:{host_port}"
|
||||||
option.status_backend_port_range.remove(host_port)
|
option.status_backend_port_range.remove(host_port)
|
||||||
|
|
||||||
|
self.base_url = url
|
||||||
self.api_url = f"{url}/statusgo"
|
self.api_url = f"{url}/statusgo"
|
||||||
self.ws_url = f"{url}".replace("http", "ws")
|
self.ws_url = f"{url}".replace("http", "ws")
|
||||||
self.rpc_url = f"{url}/statusgo/CallRPC"
|
self.rpc_url = f"{url}/statusgo/CallRPC"
|
||||||
|
@ -36,7 +39,7 @@ class StatusBackend(RpcClient, SignalClient):
|
||||||
RpcClient.__init__(self, self.rpc_url)
|
RpcClient.__init__(self, self.rpc_url)
|
||||||
SignalClient.__init__(self, self.ws_url, await_signals)
|
SignalClient.__init__(self, self.ws_url, await_signals)
|
||||||
|
|
||||||
self._health_check()
|
self.wait_for_healthy()
|
||||||
|
|
||||||
websocket_thread = threading.Thread(target=self._connect)
|
websocket_thread = threading.Thread(target=self._connect)
|
||||||
websocket_thread.daemon = True
|
websocket_thread.daemon = True
|
||||||
|
@ -72,7 +75,6 @@ class StatusBackend(RpcClient, SignalClient):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ:
|
if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ:
|
||||||
container_args["user"] = os.environ["FUNCTIONAL_TESTS_DOCKER_UID"]
|
container_args["user"] = os.environ["FUNCTIONAL_TESTS_DOCKER_UID"]
|
||||||
|
|
||||||
|
@ -84,23 +86,29 @@ class StatusBackend(RpcClient, SignalClient):
|
||||||
option.status_backend_containers.append(container.id)
|
option.status_backend_containers.append(container.id)
|
||||||
return container
|
return container
|
||||||
|
|
||||||
def _health_check(self):
|
def wait_for_healthy(self, timeout=10):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while True:
|
while time.time() - start_time <= timeout:
|
||||||
try:
|
try:
|
||||||
self.api_valid_request(method="Fleets", data=[])
|
self.health(enable_logging=False)
|
||||||
break
|
logging.info(f"StatusBackend is healthy after {time.time() - start_time} seconds")
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if time.time() - start_time > 20:
|
time.sleep(0.1)
|
||||||
raise Exception(e)
|
raise TimeoutError(
|
||||||
time.sleep(1)
|
f"StatusBackend was not healthy after {timeout} seconds")
|
||||||
|
|
||||||
def api_request(self, method, data, url=None):
|
def health(self, enable_logging=True):
|
||||||
|
return self.api_request("health", data=[], url=self.base_url, enable_logging=enable_logging)
|
||||||
|
|
||||||
|
def api_request(self, method, data, url=None, enable_logging=True):
|
||||||
url = url if url else self.api_url
|
url = url if url else self.api_url
|
||||||
url = f"{url}/{method}"
|
url = f"{url}/{method}"
|
||||||
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
|
if enable_logging:
|
||||||
|
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
|
||||||
response = requests.post(url, json=data)
|
response = requests.post(url, json=data)
|
||||||
logging.info(f"Got response: {response.content}")
|
if enable_logging:
|
||||||
|
logging.info(f"Got response: {response.content}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def verify_is_valid_api_response(self, response):
|
def verify_is_valid_api_response(self, response):
|
||||||
|
@ -115,8 +123,8 @@ class StatusBackend(RpcClient, SignalClient):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def api_valid_request(self, method, data):
|
def api_valid_request(self, method, data, url=None):
|
||||||
response = self.api_request(method, data)
|
response = self.api_request(method, data, url)
|
||||||
self.verify_is_valid_api_response(response)
|
self.verify_is_valid_api_response(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@ class TestInitialiseApp:
|
||||||
backend_client.restore_account_and_login()
|
backend_client.restore_account_and_login()
|
||||||
|
|
||||||
assert backend_client is not None
|
assert backend_client is not None
|
||||||
|
backend_client.verify_json_schema(
|
||||||
|
backend_client.wait_for_login(),
|
||||||
|
"signal_node_login",
|
||||||
|
)
|
||||||
backend_client.verify_json_schema(
|
backend_client.verify_json_schema(
|
||||||
backend_client.wait_for_signal(SignalType.MEDIASERVER_STARTED.value),
|
backend_client.wait_for_signal(SignalType.MEDIASERVER_STARTED.value),
|
||||||
"signal_mediaserver_started",
|
"signal_mediaserver_started",
|
||||||
|
@ -35,7 +39,6 @@ class TestInitialiseApp:
|
||||||
backend_client.wait_for_signal(SignalType.NODE_READY.value),
|
backend_client.wait_for_signal(SignalType.NODE_READY.value),
|
||||||
"signal_node_ready",
|
"signal_node_ready",
|
||||||
)
|
)
|
||||||
backend_client.verify_json_schema(backend_client.wait_for_login(), "signal_node_login")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.rpc
|
@pytest.mark.rpc
|
||||||
|
|
Loading…
Reference in New Issue