diff --git a/test/nodetool/test_sstable.py b/test/nodetool/test_sstable.py index 5949e5e535..a9926975d4 100644 --- a/test/nodetool/test_sstable.py +++ b/test/nodetool/test_sstable.py @@ -53,3 +53,199 @@ def test_getsstables_unknown_tbl(nodetool): ["error processing arguments: unknown table: unknown_tbl", "ColumnFamilyStore for ks/unknown_tbl not found."] ) + + +ks_tbl_sstable_info = { + "keyspace": "ks", + "table": "tbl", + "sstables": [ + { + "size": 5746, + "data_size": 90, + "index_size": 24, + "filter_size": 332, + "timestamp": "2024-03-11T08:13:19Z", + "generation": "3gec_0mu7_5az0024x96bfm476r6", + "level": 0, + "version": "me", + "extended_properties": [ + { + "group": "compression_parameters", + "attributes": [ + { + "key": "sstable_compression", + "value": "org.apache.cassandra.io.compress.LZ4Compressor" + } + ] + } + ] + }, + { + "size": 6746, + "data_size": 290, + "index_size": 124, + "filter_size": 232, + "timestamp": "2024-03-10T08:13:19Z", + "generation": "3gec_0mu7_6bz0024x96bfm476r6", + "level": 0, + "version": "me", + "properties": [ + { + "key": "foo", + "value": "bar" + } + ] + } + ] +} + + +ks_tbl2_sstable_info = { + "keyspace": "ks", + "table": "tbl2", + "sstables": [ + { + "size": 5481, + "data_size": 44, + "index_size": 8, + "filter_size": 172, + "timestamp": "2024-03-11T08:13:20Z", + "generation": "3gec_0mu8_5vrgh24x96bfm476r6", + "level": 0, + "version": "me", + "extended_properties": [ + { + "group": "compression_parameters", + "attributes": [ + { + "key": "sstable_compression", + "value": "org.apache.cassandra.io.compress.LZ4Compressor" + } + ] + } + ] + } + ] +} + + +ks2_tbl_sstable_info = { + "keyspace": "ks2", + "table": "tbl" +} + + +def _check_sstableinfo_output(res, info, is_cassandra): + lines = res.split('\n') + i = 0 + + assert lines[i] == '' + i += 1 + + def split(ln): + return tuple(part.strip() for part in ln.split(':')) + + for entry in info: + if "sstables" not in entry and is_cassandra: + i += 2 + else: + assert split(lines[i]) == ("keyspace", entry["keyspace"]) + i += 1 + + assert split(lines[i]) == ("table", entry["table"]) + i += 1 + + if "sstables" in entry: + assert lines[i] == "sstables :" + i += 1 + else: + continue + + for index, sstable in enumerate(entry["sstables"]): + assert lines[i].lstrip() == f"{index} :" + i += 1 + + for key in ["data_size", "filter_size", "index_size", "level", "size", "generation", "version", + "timestamp"]: + print_key = key.replace("_", " ") + if print_key == "timestamp": + parts = split(lines[i]) + assert parts[0] == print_key + # Java nodetool does a weird reformatting of the date which I see no sense in replicating + if not is_cassandra: + assert ":".join(parts[1:]) == sstable[key] + else: + assert split(lines[i]) == (print_key, str(sstable[key])) + i += 1 + + if "properties" in sstable: + assert lines[i].strip() == "properties :" + i += 1 + + for prop in sstable["properties"]: + assert split(lines[i]) == (prop["key"], prop["value"]) + i += 1 + + if "extended_properties" in sstable: + assert lines[i].strip() == "extended properties :" + i += 1 + + for ext_prop in sstable["extended_properties"]: + assert lines[i].strip() == f"{ext_prop['group']} :" + i += 1 + + for attr in ext_prop["attributes"]: + assert split(lines[i]) == (attr["key"], attr["value"]) + i += 1 + + +def test_sstableinfo(nodetool, request): + info = [ks_tbl_sstable_info, ks_tbl2_sstable_info, ks2_tbl_sstable_info] + res = nodetool("sstableinfo", expected_requests=[ + expected_request( + "GET", + "/storage_service/sstable_info", + response=info), + ]) + _check_sstableinfo_output(res, info, request.config.getoption("nodetool") == "cassandra") + + +def test_sstableinfo_keyspace(nodetool, request): + info = [ks_tbl_sstable_info, ks_tbl2_sstable_info] + res = nodetool("sstableinfo", "ks", expected_requests=[ + expected_request( + "GET", + "/storage_service/sstable_info", + params={"keyspace": "ks"}, + response=info), + ]) + _check_sstableinfo_output(res, info, request.config.getoption("nodetool") == "cassandra") + + +def test_sstableinfo_keyspace_table(nodetool, request): + info = [ks_tbl_sstable_info] + res = nodetool("sstableinfo", "ks", "tbl", expected_requests=[ + expected_request( + "GET", + "/storage_service/sstable_info", + params={"keyspace": "ks", "cf": "tbl"}, + response=info), + ]) + _check_sstableinfo_output(res, info, request.config.getoption("nodetool") == "cassandra") + + +def test_sstableinfo_keyspace_tables(nodetool, request): + info = [ks_tbl_sstable_info, ks_tbl2_sstable_info] + res = nodetool("sstableinfo", "ks", "tbl", "tbl2", expected_requests=[ + expected_request( + "GET", + "/storage_service/sstable_info", + params={"keyspace": "ks", "cf": "tbl"}, + response=[ks_tbl_sstable_info]), + expected_request( + "GET", + "/storage_service/sstable_info", + params={"keyspace": "ks", "cf": "tbl2"}, + response=[ks_tbl2_sstable_info]), + ]) + _check_sstableinfo_output(res, info, request.config.getoption("nodetool") == "cassandra") diff --git a/tools/scylla-nodetool.cc b/tools/scylla-nodetool.cc index 8f7800f600..23cb13b48c 100644 --- a/tools/scylla-nodetool.cc +++ b/tools/scylla-nodetool.cc @@ -1800,6 +1800,75 @@ void snapshot_operation(scylla_rest_client& client, const bpo::variables_map& vm fmt::print(std::cout, "Snapshot directory: {}\n", params["tag"]); } +void sstableinfo_operation(scylla_rest_client& client, const bpo::variables_map& vm) { + std::vector requests; + if (vm.count("table")) { + const auto keyspace = vm["keyspace"].as(); + for (const auto& table : vm["table"].as>()) { + requests.push_back(keyspace_and_table{.keyspace = keyspace, .table = table}); + } + } else if (vm.count("keyspace")) { + requests.push_back(keyspace_and_table{.keyspace = vm["keyspace"].as()}); + } else { + requests.push_back(keyspace_and_table{}); + } + + fmt::print("\n"); + + for (const auto& req : requests) { + std::unordered_map params; + if (!req.keyspace.empty()) { + params["keyspace"] = req.keyspace; + } + if (!req.table.empty()) { + params["cf"] = req.table; + } + auto res = client.get("/storage_service/sstable_info", std::move(params)); + + for (const auto& entry : res.GetArray()) { + fmt::print("{:>8} : {}\n", "keyspace", rjson::to_string_view(entry["keyspace"])); + fmt::print("{:>8} : {}\n", "table", rjson::to_string_view(entry["table"])); + if (!entry.HasMember("sstables")) { + continue; + } + fmt::print("sstables :\n"); + unsigned i = 0; + for (const auto& sstable : entry["sstables"].GetArray()) { + fmt::print("{:>8} :\n", i++); + + // Keep the same order as the Java nodetool. + // NOTE: we keep the timestamp field as-is, while the Java nodetool re-formats it. + for (const auto& key : {"data_size", "filter_size", "index_size", "level", "size", "generation", "version", "timestamp"}) { + std::string print_key = key; + std::replace(print_key.begin(), print_key.end(), '_', ' '); + if (sstable[key].IsNumber()) { + fmt::print("{:>23} : {}\n", print_key, sstable[key]); + } else { + fmt::print("{:>23} : {}\n", print_key, rjson::to_string_view(sstable[key])); + } + } + + if (sstable.HasMember("properties")) { + fmt::print("{:>23} :\n", "properties"); + for (const auto& property : sstable["properties"].GetArray()) { + fmt::print("{:>16} : {}\n", rjson::to_string_view(property["key"]), rjson::to_string_view(property["value"])); + } + } + + if (sstable.HasMember("extended_properties")) { + fmt::print("{:>23} :\n", "extended properties"); + for (const auto& extended_property : sstable["extended_properties"].GetArray()) { + fmt::print("{:>35} :\n", rjson::to_string_view(extended_property["group"])); + for (const auto& attribute : extended_property["attributes"].GetArray()) { + fmt::print("{:>43} : {}\n", rjson::to_string_view(attribute["key"]), rjson::to_string_view(attribute["value"])); + } + } + } + } + } + } +} + static bool keyspace_uses_tablets(scylla_rest_client& client, const sstring& keyspace) { const std::unordered_map params = {{"replication", "tablets"}}; const auto res = client.get("/storage_service/keyspaces", params); @@ -3353,6 +3422,20 @@ Fore more information, see: https://opensource.docs.scylladb.com/stable/operatin }, snapshot_operation }, + { + { + "sstableinfo", + "Information about sstables per keyspace/table", +R"( +)", + { }, + { + typed_option("keyspace", "The keyspace name", 1), + typed_option>("table", "The table names (optional)", -1), + }, + }, + sstableinfo_operation, + }, { { "status",