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:
Igor Sirotin 2024-12-17 15:37:53 +00:00 committed by GitHub
parent 6a5623bac6
commit 309d17ae5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 76 additions and 16 deletions

View File

@ -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.

View File

@ -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
}
}

View File

@ -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

View File

@ -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
```

View File

@ -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

View File

@ -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