From ff91dcef95bf4dd043d1ac2a86603c8d87a4559b Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 20 Nov 2024 18:14:58 -0300 Subject: [PATCH] feat: scylladb database provider implementation (#320) --- .github/workflows/generate-linter-core.yml | 2 +- .github/workflows/testcontainers.yml | 2 +- README.md | 1 + cmd/flags/database.go | 3 +- cmd/program/program.go | 21 +++ cmd/steps/steps.go | 3 + cmd/template/dbdriver/files/env/scylla.tmpl | 10 ++ .../dbdriver/files/service/scylla.tmpl | 165 ++++++++++++++++++ cmd/template/dbdriver/files/tests/scylla.tmpl | 135 ++++++++++++++ cmd/template/dbdriver/scylla.go | 28 +++ .../docker/files/docker-compose/scylla.tmpl | 70 ++++++++ cmd/template/docker/scylla.go | 14 ++ cmd/utils/utils.go | 13 ++ docs/docs/blueprint-core/db-drivers.md | 1 + docs/docs/endpoints-test/scylladb.md | 154 ++++++++++++++++ docs/mkdocs.yml | 1 + 16 files changed, 620 insertions(+), 3 deletions(-) create mode 100644 cmd/template/dbdriver/files/env/scylla.tmpl create mode 100644 cmd/template/dbdriver/files/service/scylla.tmpl create mode 100644 cmd/template/dbdriver/files/tests/scylla.tmpl create mode 100644 cmd/template/dbdriver/scylla.go create mode 100644 cmd/template/docker/files/docker-compose/scylla.tmpl create mode 100644 cmd/template/docker/scylla.go create mode 100644 docs/docs/endpoints-test/scylladb.md diff --git a/.github/workflows/generate-linter-core.yml b/.github/workflows/generate-linter-core.yml index e3774478..2d67a960 100644 --- a/.github/workflows/generate-linter-core.yml +++ b/.github/workflows/generate-linter-core.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: framework: [chi, gin, fiber, gorilla/mux, httprouter, standard-library, echo] - driver: [mysql, postgres, sqlite, mongo, redis, none] + driver: [mysql, postgres, sqlite, mongo, redis, scylla, none] git: [commit, stage, skip] runs-on: ubuntu-latest diff --git a/.github/workflows/testcontainers.yml b/.github/workflows/testcontainers.yml index c924b3fa..e632b753 100644 --- a/.github/workflows/testcontainers.yml +++ b/.github/workflows/testcontainers.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: driver: - [mysql, postgres, mongo, redis] + [mysql, postgres, mongo, redis, scylla] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 4e00b19b..3db4de72 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Choose from a variety of supported database drivers: - [Sqlite](https://github.com/mattn/go-sqlite3) - [Mongo](https://go.mongodb.org/mongo-driver) - [Redis](https://github.com/redis/go-redis) +- [ScyllaDB GoCQL](https://github.com/scylladb/gocql) diff --git a/cmd/flags/database.go b/cmd/flags/database.go index 7fd87fb5..8f7e0741 100644 --- a/cmd/flags/database.go +++ b/cmd/flags/database.go @@ -16,10 +16,11 @@ const ( Sqlite Database = "sqlite" Mongo Database = "mongo" Redis Database = "redis" + Scylla Database = "scylla" None Database = "none" ) -var AllowedDBDrivers = []string{string(MySql), string(Postgres), string(Sqlite), string(Mongo), string(Redis), string(None)} +var AllowedDBDrivers = []string{string(MySql), string(Postgres), string(Sqlite), string(Mongo), string(Redis), string(Scylla), string(None)} func (f Database) String() string { return string(f) diff --git a/cmd/program/program.go b/cmd/program/program.go index b2491852..37af63d2 100644 --- a/cmd/program/program.go +++ b/cmd/program/program.go @@ -104,6 +104,8 @@ var ( sqliteDriver = []string{"github.com/mattn/go-sqlite3"} redisDriver = []string{"github.com/redis/go-redis/v9"} mongoDriver = []string{"go.mongodb.org/mongo-driver"} + gocqlDriver = []string{"github.com/gocql/gocql"} + scyllaDriver = "github.com/scylladb/gocql@v1.14.4" // Replacement for GoCQL godotenvPackage = []string{"github.com/joho/godotenv"} templPackage = []string{"github.com/a-h/templ"} @@ -206,6 +208,11 @@ func (p *Project) createDBDriverMap() { packageName: redisDriver, templater: dbdriver.RedisTemplate{}, } + + p.DBDriverMap[flags.Scylla] = Driver{ + packageName: gocqlDriver, + templater: dbdriver.ScyllaTemplate{}, + } } func (p *Project) createDockerMap() { @@ -227,6 +234,10 @@ func (p *Project) createDockerMap() { packageName: []string{}, templater: docker.RedisDockerTemplate{}, } + p.DockerMap[flags.Scylla] = Docker{ + packageName: []string{}, + templater: docker.ScyllaDockerTemplate{}, + } } // CreateMainFile creates the project folders and files, @@ -335,12 +346,22 @@ func (p *Project) CreateMainFile() error { // Install the godotenv package err = utils.GoGetPackage(projectPath, godotenvPackage) + if err != nil { log.Printf("Could not install go dependency %v\n", err) return err } + if p.DBDriver == flags.Scylla { + replace := fmt.Sprintf("%s=%s", gocqlDriver[0], scyllaDriver) + err = utils.GoModReplace(projectPath, replace) + if err != nil { + log.Printf("Could not replace go dependency %v\n", err) + return err + } + } + err = p.CreatePath(cmdApiPath, projectPath) if err != nil { log.Printf("Error creating path: %s", projectPath) diff --git a/cmd/steps/steps.go b/cmd/steps/steps.go index b11d421f..eacb874b 100644 --- a/cmd/steps/steps.go +++ b/cmd/steps/steps.go @@ -82,6 +82,9 @@ func InitSteps(projectType flags.Framework, databaseType flags.Database) *Steps { Title: "Redis", Desc: "Redis driver for Go."}, + { + Title: "Scylla", + Desc: "ScyllaDB Enhanced driver from GoCQL."}, { Title: "None", Desc: "Choose this option if you don't wish to install a specific database driver."}, diff --git a/cmd/template/dbdriver/files/env/scylla.tmpl b/cmd/template/dbdriver/files/env/scylla.tmpl new file mode 100644 index 00000000..bebd42c0 --- /dev/null +++ b/cmd/template/dbdriver/files/env/scylla.tmpl @@ -0,0 +1,10 @@ +{{- if .AdvancedOptions.docker }} +# BLUEPRINT_DB_HOSTS=scylla_bp:9042 # ScyllaDB default port +BLUEPRINT_DB_HOSTS=scylla_bp:19042 # ScyllaDB Shard-Aware port +{{- else }} +# BLUEPRINT_DB_HOSTS=localhost:9042 # ScyllaDB default port +BLUEPRINT_DB_HOSTS=localhost:19042 # ScyllaDB Shard-Aware port +{{- end }} +BLUEPRINT_DB_CONSISTENCY="LOCAL_QUORUM" +# BLUEPRINT_DB_USERNAME= +# BLUEPRINT_DB_PASSWORD= \ No newline at end of file diff --git a/cmd/template/dbdriver/files/service/scylla.tmpl b/cmd/template/dbdriver/files/service/scylla.tmpl new file mode 100644 index 00000000..c27482c0 --- /dev/null +++ b/cmd/template/dbdriver/files/service/scylla.tmpl @@ -0,0 +1,165 @@ +package database + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/gocql/gocql" + _ "github.com/joho/godotenv/autoload" +) + +// Service defines the interface for health checks. +type Service interface { + Health() map[string]string + Close() error +} + +// service implements the Service interface. +type service struct { + Session *gocql.Session +} + +// Environment variables for ScyllaDB connection. +var ( + hosts = os.Getenv("BLUEPRINT_DB_HOSTS") // Comma-separated list of hosts:port + username = os.Getenv("BLUEPRINT_DB_USERNAME") // Username for authentication + password = os.Getenv("BLUEPRINT_DB_PASSWORD") // Password for authentication + consistencyLevel = os.Getenv("BLUEPRINT_DB_CONSISTENCY") // Consistency level +) + +// New initializes a new Service with a ScyllaDB Session. +func New() Service { + cluster := gocql.NewCluster(strings.Split(hosts, ",")...) + cluster.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy(gocql.RoundRobinHostPolicy()) + + // Set authentication if provided + if username != "" && password != "" { + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: username, + Password: password, + } + } + + // Set consistency level if provided + if consistencyLevel != "" { + if cl, err := parseConsistency(consistencyLevel); err == nil { + cluster.Consistency = cl + } else { + log.Printf("Invalid SCYLLA_DB_CONSISTENCY '%s', using default: %v", consistencyLevel, err) + } + } + + // Create Session + session, err := cluster.CreateSession() + if err != nil { + log.Fatalf("Failed to connect to ScyllaDB cluster: %v", err) + } + + s := &service{Session: session} + return s +} + +// parseConsistency converts a string to a gocql.Consistency value. +func parseConsistency(cons string) (gocql.Consistency, error) { + consistencyMap := map[string]gocql.Consistency{ + "ANY": gocql.Any, + "ONE": gocql.One, + "TWO": gocql.Two, + "THREE": gocql.Three, + "QUORUM": gocql.Quorum, + "ALL": gocql.All, + "LOCAL_ONE": gocql.LocalOne, + "LOCAL_QUORUM": gocql.LocalQuorum, + "EACH_QUORUM": gocql.EachQuorum, + } + + if consistency, ok := consistencyMap[strings.ToUpper(cons)]; ok { + return consistency, nil + } + return gocql.LocalQuorum, fmt.Errorf("unknown consistency level: %s", cons) +} + +// Health returns the health status and statistics of the ScyllaDB cluster. +func (s *service) Health() map[string]string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stats := make(map[string]string) + + // Check ScyllaDB health and populate the stats map + startedAt := time.Now() + + // Execute a simple query to check connectivity + query := "SELECT now() FROM system.local" + iter := s.Session.Query(query).WithContext(ctx).Iter() + var currentTime time.Time + if !iter.Scan(¤tTime) { + if err := iter.Close(); err != nil { + stats["status"] = "down" + stats["message"] = fmt.Sprintf("Failed to execute query: %v", err) + return stats + } + } + if err := iter.Close(); err != nil { + stats["status"] = "down" + stats["message"] = fmt.Sprintf("Error during query execution: %v", err) + return stats + } + + // ScyllaDB is up + stats["status"] = "up" + stats["message"] = "It's healthy" + stats["scylla_current_time"] = currentTime.String() + + // Retrieve cluster information + // Get keyspace information + getKeyspacesQuery := "SELECT keyspace_name FROM system_schema.keyspaces" + keyspacesIterator := s.Session.Query(getKeyspacesQuery).Iter() + + stats["scylla_keyspaces"] = strconv.Itoa(keyspacesIterator.NumRows()) + if err := keyspacesIterator.Close(); err != nil { + log.Fatalf("Failed to close keyspaces iterator: %v", err) + } + + // Get cluster information + var currentDatacenter string + var currentHostStatus bool + + var clusterNodesUp uint + var clusterNodesDown uint + var clusterSize uint + + clusterNodesIterator := s.Session.Query("SELECT dc, up FROM system.cluster_status").Iter() + for clusterNodesIterator.Scan(¤tDatacenter, ¤tHostStatus) { + clusterSize++ + if currentHostStatus { + clusterNodesUp++ + } else { + clusterNodesDown++ + } + } + + if err := clusterNodesIterator.Close(); err != nil { + log.Fatalf("Failed to close cluster nodes iterator: %v", err) + } + + stats["scylla_cluster_size"] = strconv.Itoa(int(clusterSize)) + stats["scylla_cluster_nodes_up"] = strconv.Itoa(int(clusterNodesUp)) + stats["scylla_cluster_nodes_down"] = strconv.Itoa(int(clusterNodesDown)) + stats["scylla_current_datacenter"] = currentDatacenter + + // Calculate the time taken to perform the health check + stats["scylla_health_check_duration"] = time.Since(startedAt).String() + return stats +} + +// Close gracefully closes the ScyllaDB Session. +func (s *service) Close() error { + s.Session.Close() + return nil +} \ No newline at end of file diff --git a/cmd/template/dbdriver/files/tests/scylla.tmpl b/cmd/template/dbdriver/files/tests/scylla.tmpl new file mode 100644 index 00000000..9b4d9241 --- /dev/null +++ b/cmd/template/dbdriver/files/tests/scylla.tmpl @@ -0,0 +1,135 @@ +package database + +import ( + "context" + "fmt" + "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "io" + "log" + "strings" + "testing" +) + +const ( + port = nat.Port("19042/tcp") +) + +func mustStartScyllaDBContainer() (testcontainers.Container, error) { + + // Define the container + image := "scylladb/scylla:6.2" + exposedPorts := []string{"9042/tcp", "19042/tcp"} + commands := []string{ + "--smp=2", + "--memory=1G", + "--developer-mode=1", + "--overprovisioned=1", + } + + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{}, + Image: image, + ExposedPorts: exposedPorts, + Cmd: commands, + WaitingFor: wait.ForAll( + wait.ForLog(".*initialization completed.").AsRegexp(), + wait.ForListeningPort(port), + wait.ForExec([]string{"cqlsh", "-e", "SELECT bootstrapped FROM system.local"}).WithResponseMatcher(func(body io.Reader) bool { + data, _ := io.ReadAll(body) + return strings.Contains(string(data), "COMPLETED") + }), + ), + } + + // Start the container + scyllaDBContainer, err := testcontainers.GenericContainer( + context.Background(), testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, err + } + + mappedPort, err := scyllaDBContainer.MappedPort(context.Background(), "19042/tcp") + if err != nil { + return nil, err + } + + hosts = fmt.Sprintf("localhost:%v", mappedPort.Port()) + + return scyllaDBContainer, nil +} + +func TestMain(m *testing.M) { + + container, err := mustStartScyllaDBContainer() + if err != nil { + log.Fatalf("could not start scylla container: %v", err) + } + + m.Run() + + err = container.Terminate(context.Background()) + if err != nil { + return + } + +} + +func TestNew(t *testing.T) { + srv := New() + if srv == nil { + t.Fatal("New() returned nil") + } + + err := srv.Close() + if err != nil { + t.Fatalf("expected Close() to return nil") + } +} + +func TestHealth(t *testing.T) { + srv := New() + + stats := srv.Health() + + if stats["status"] != "up" { + t.Fatalf("expected status to be up, got %s", stats["status"]) + } + + if _, ok := stats["error"]; ok { + t.Fatalf("expected error not to be present") + } + + if stats["message"] != "It's healthy" { + t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) + } + + if stats["scylla_cluster_nodes_up"] != "1" { + t.Fatalf("expected nodes up '1', got %s", stats["scylla_cluster_nodes_up"]) + } + + if stats["scylla_cluster_nodes_down"] != "0" { + t.Fatalf("expected nodes down '0', got %s", stats["scylla_cluster_nodes_down"]) + } + + if stats["scylla_current_datacenter"] != "datacenter1" { + t.Fatalf("expected connected dc 'datacenter', got %s", stats["scylla_current_datacenter"]) + } + + err := srv.Close() + if err != nil { + t.Fatalf("expected Close() to return nil") + } +} + +func TestClose(t *testing.T) { + srv := New() + + if srv.Close() != nil { + t.Fatalf("expected Close() to return nil") + } +} diff --git a/cmd/template/dbdriver/scylla.go b/cmd/template/dbdriver/scylla.go new file mode 100644 index 00000000..35b681a5 --- /dev/null +++ b/cmd/template/dbdriver/scylla.go @@ -0,0 +1,28 @@ +package dbdriver + +import ( + _ "embed" +) + +type ScyllaTemplate struct{} + +//go:embed files/service/scylla.tmpl +var scyllaServiceTemplate []byte + +//go:embed files/env/scylla.tmpl +var scyllaEnvTemplate []byte + +//go:embed files/tests/scylla.tmpl +var scyllaTestcontainersTemplate []byte + +func (r ScyllaTemplate) Service() []byte { + return scyllaServiceTemplate +} + +func (r ScyllaTemplate) Env() []byte { + return scyllaEnvTemplate +} + +func (r ScyllaTemplate) Tests() []byte { + return scyllaTestcontainersTemplate +} diff --git a/cmd/template/docker/files/docker-compose/scylla.tmpl b/cmd/template/docker/files/docker-compose/scylla.tmpl new file mode 100644 index 00000000..682a0193 --- /dev/null +++ b/cmd/template/docker/files/docker-compose/scylla.tmpl @@ -0,0 +1,70 @@ +services: +{{- if .AdvancedOptions.docker }} + app: + build: + context: . + dockerfile: Dockerfile + target: prod + restart: unless-stopped + ports: + - ${PORT}:${PORT} + environment: + APP_ENV: ${APP_ENV} + PORT: ${PORT} + BLUEPRINT_DB_HOSTS: ${BLUEPRINT_DB_HOSTS} + BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} + BLUEPRINT_DB_CONSISTENCY: ${BLUEPRINT_DB_CONSISTENCY} + BLUEPRINT_DB_KEYSPACE: ${BLUEPRINT_DB_KEYSPACE} + BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} + BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} + BLUEPRINT_DB_CONNECTIONS: ${BLUEPRINT_DB_CONNECTIONS} + depends_on: + scylla_bp: + condition: service_healthy + networks: + - blueprint +{{- end }} +{{- if and .AdvancedOptions.react .AdvancedOptions.docker }} + frontend: + build: + context: . + dockerfile: Dockerfile + target: frontend + restart: unless-stopped + depends_on: + - app + ports: + - 5173:5173 + networks: + - blueprint +{{- end }} + scylla_bp: + image: scylladb/scylla:6.2 + restart: unless-stopped + command: + - --smp=2 + - --memory=1GB + - --overprovisioned=1 + - --developer-mode=1 # Disable for production + - --seeds=scylla_bp + ports: + - "9042:9042" + - "19042:19042" + volumes: + - scylla_bp:/var/lib/scylla + {{- if .AdvancedOptions.docker }} + healthcheck: + test: ["CMD-SHELL", 'cqlsh -e "SHOW VERSION" || exit 1'] + interval: 15s + timeout: 30s + retries: 15 + start_period: 30s + networks: + - blueprint + {{- end }} +volumes: + scylla_bp: +{{- if .AdvancedOptions.docker }} +networks: + blueprint: +{{- end }} \ No newline at end of file diff --git a/cmd/template/docker/scylla.go b/cmd/template/docker/scylla.go new file mode 100644 index 00000000..b4815f2f --- /dev/null +++ b/cmd/template/docker/scylla.go @@ -0,0 +1,14 @@ +package docker + +import ( + _ "embed" +) + +type ScyllaDockerTemplate struct{} + +//go:embed files/docker-compose/scylla.tmpl +var scyllaDockerTemplate []byte + +func (r ScyllaDockerTemplate) Docker() []byte { + return scyllaDockerTemplate +} diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 9a1f9bc2..368d935c 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -97,6 +97,19 @@ func GoFmt(appDir string) error { return nil } +// GoModReplace runs "go mod edit -replace" in the selected +// replace_payload e.g: github.com/gocql/gocql=github.com/scylladb/gocql@v1.14.4 +func GoModReplace(appDir string, replace string) error { + if err := ExecuteCmd("go", + []string{"mod", "edit", "-replace", replace}, + appDir, + ); err != nil { + return err + } + + return nil +} + func GoTidy(appDir string) error { err := ExecuteCmd("go", []string{"mod", "tidy"}, appDir) if err != nil { diff --git a/docs/docs/blueprint-core/db-drivers.md b/docs/docs/blueprint-core/db-drivers.md index b4e76e65..f34c72d0 100644 --- a/docs/docs/blueprint-core/db-drivers.md +++ b/docs/docs/blueprint-core/db-drivers.md @@ -5,6 +5,7 @@ To extend the project with database functionality, users can choose from a varie 3. [Postgres](https://github.com/jackc/pgx/): Facilitates connectivity to PostgreSQL databases. 4. [Redis](https://github.com/redis/go-redis): Provides tools for connecting and interacting with Redis. 5. [Sqlite](https://github.com/mattn/go-sqlite3): Suitable for projects requiring a lightweight, self-contained database. and interacting with Redis +6. [ScyllaDB](https://github.com/scylladb/gocql): Facilitates connectivity to ScyllaDB databases. ## Updated Project Structure diff --git a/docs/docs/endpoints-test/scylladb.md b/docs/docs/endpoints-test/scylladb.md new file mode 100644 index 00000000..faface71 --- /dev/null +++ b/docs/docs/endpoints-test/scylladb.md @@ -0,0 +1,154 @@ +To test the ScyllaDB Health Check endpoint, use the following curl command: + +```bash +curl http://localhost:PORT/health +``` + +## Health Function + +The `Health` function checks the health of the ScyllaDB Cluster by pinging +the [Coordinator Node](https://opensource.docs.scylladb.com/stable/architecture/architecture-fault-tolerance.html). It +returns a simple map containing a health message. + +### Functionality + +**Ping ScyllaDB Server**: The function pings the ScyllaDB through server to check its availability. + +- If the ping fails, it logs the error and terminates the program. +- If the ping succeeds, it returns a health message indicating that the server with some . + +### Sample Output + +The `Health` returns a JSON-like map structure with a single key indicating the health status: + +```json +{ + "message": "It's healthy", + "status": "up", + "scylla_cluster_nodes_up": "3", + "scylla_cluster_nodes_down": "0", + "scylla_cluster_size": "1", + "scylla_current_datacenter": "datacenter1", + "scylla_current_time": "2024-11-04 22:59:21.69 +0000 UTC", + "scylla_health_check_duration": "16.896976ms", + "scylla_keyspaces": "6" +} +``` + +## ScyllaDB Setup + +Before starting the cluster, ensure the [fs.aio-max-nr](https://www.kernel.org/doc/Documentation/sysctl/fs.txt) value is +sufficient (e.g. `1048576` or `2097152` or more). + +If you prefer to configure it manually, run one of the following commands to check the current value: + +```sh +sysctl --all | grep --word-regexp -- 'aio-max-nr' +``` + +```sh +sysctl fs.aio-max-nr +``` + +```sh +cat /proc/sys/fs/aio-max-nr +``` + +If the value is lower than required, you can use one of these commands: + +```sh +# Update config non-persistent +sysctl --write fs.aio-max-nr=1048576 +``` + +Here's some links for more relevant information and automation: + +* [Repository: gvieira/ws-scylla](https://github.com/gvieira18/ws-scylla/) - Simple ScyllaDB Cluster magement with + Makefiles +* [ScyllaDB University: 101 Essentials Track](https://university.scylladb.com/courses/scylla-essentials-overview) - + Learn the base concepts of ScyllaDB + +## Code implementation + +Here you can check how the Health Check is done under the hood: + +```go +func (s *service) Health() map[string]string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stats := make(map[string]string) + + // Check ScyllaDB health and populate the stats map + startedAt := time.Now() + + // Execute a simple query to check connectivity + query := "SELECT now() FROM system.local" + iter := s.Session.Query(query).WithContext(ctx).Iter() + var currentTime time.Time + if !iter.Scan(¤tTime) { + if err := iter.Close(); err != nil { + stats["status"] = "down" + stats["message"] = fmt.Sprintf("Failed to execute query: %v", err) + return stats + } + } + if err := iter.Close(); err != nil { + stats["status"] = "down" + stats["message"] = fmt.Sprintf("Error during query execution: %v", err) + return stats + } + + // ScyllaDB is up + stats["status"] = "up" + stats["message"] = "It's healthy" + stats["scylla_current_time"] = currentTime.String() + + // Retrieve cluster information + // Get keyspace information + getKeyspacesQuery := "SELECT keyspace_name FROM system_schema.keyspaces" + keyspacesIterator := s.Session.Query(getKeyspacesQuery).Iter() + + stats["scylla_keyspaces"] = strconv.Itoa(keyspacesIterator.NumRows()) + if err := keyspacesIterator.Close(); err != nil { + log.Fatalf("Failed to close keyspaces iterator: %v", err) + } + + // Get cluster information + var currentDatacenter string + var currentHostStatus bool + + var clusterNodesUp uint + var clusterNodesDown uint + var clusterSize uint + + clusterNodesIterator := s.Session.Query("SELECT dc, up FROM system.cluster_status").Iter() + for clusterNodesIterator.Scan(¤tDatacenter, ¤tHostStatus) { + clusterSize++ + if currentHostStatus { + clusterNodesUp++ + } else { + clusterNodesDown++ + } + } + + if err := clusterNodesIterator.Close(); err != nil { + log.Fatalf("Failed to close cluster nodes iterator: %v", err) + } + + stats["scylla_cluster_size"] = strconv.Itoa(int(clusterSize)) + stats["scylla_cluster_nodes_up"] = strconv.Itoa(int(clusterNodesUp)) + stats["scylla_cluster_nodes_down"] = strconv.Itoa(int(clusterNodesDown)) + stats["scylla_current_datacenter"] = currentDatacenter + + // Calculate the time taken to perform the health check + stats["scylla_health_check_duration"] = time.Since(startedAt).String() + return stats +} + +``` + +## Note + +Scylladb does not support advanced health check functions like SQL databases or Redis. +The current implementation is based on queries at `system` related keyspaces. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6fa8a4df..5125c709 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -53,6 +53,7 @@ nav: - SQL DBs: endpoints-test/sql.md - Redis: endpoints-test/redis.md - MongoDB: endpoints-test/mongo.md + - ScyllaDB: endpoints-test/scylladb.md - Websocket: endpoints-test/websocket.md - Web Endpoint: endpoints-test/web.md