aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorinnokentii <innokentii@yandex-team.com>2022-11-21 16:03:15 +0300
committerinnokentii <innokentii@yandex-team.com>2022-11-21 16:03:15 +0300
commitc6fae4ac291a4ef63dcc0439fd6e6816937efbc5 (patch)
treeb0b27f6f7a815b37d2438f70576d808442a941c6
parentbb9fc75c7a269a63ec9966399916a0cdb05b1353 (diff)
downloadydb-c6fae4ac291a4ef63dcc0439fd6e6816937efbc5.tar.gz
Add filters to sentinel introspection page
add filters and history to sentinel viewer
-rw-r--r--ydb/core/cms/json_proxy_sentinel.h43
-rw-r--r--ydb/core/cms/sentinel.cpp117
-rw-r--r--ydb/core/cms/sentinel_impl.h4
-rw-r--r--ydb/core/cms/ui/index.html15
-rw-r--r--ydb/core/cms/ui/nanotable.js31
-rw-r--r--ydb/core/cms/ui/sentinel.css14
-rw-r--r--ydb/core/cms/ui/sentinel_state.js557
-rw-r--r--ydb/core/protos/cms.proto26
8 files changed, 562 insertions, 245 deletions
diff --git a/ydb/core/cms/json_proxy_sentinel.h b/ydb/core/cms/json_proxy_sentinel.h
index 7520765e068..a44e0adbebe 100644
--- a/ydb/core/cms/json_proxy_sentinel.h
+++ b/ydb/core/cms/json_proxy_sentinel.h
@@ -17,6 +17,49 @@ public:
TAutoPtr<TRequest> PrepareRequest(const TActorContext &) override
{
TAutoPtr<TRequest> request = new TRequest;
+ const TCgiParameters& cgi = RequestEvent->Get()->Request.GetParams();
+
+ if (cgi.Has("show")) {
+ NKikimrCms::TGetSentinelStateRequest::EShow show;
+ NKikimrCms::TGetSentinelStateRequest::EShow_Parse(cgi.Get("show"), &show);
+ request->Record.SetShow(show);
+ }
+
+ if (cgi.Has("range")) {
+ TVector<std::pair<ui32, ui32>> ranges;
+ auto rangesStr = cgi.Get("range");
+ TVector<TString> strRanges;
+ StringSplitter(rangesStr).Split(',').Collect(&strRanges);
+ for (auto& strRange : strRanges) {
+ ui32 begin = 0;
+ ui32 end = 0;
+ if (!StringSplitter(strRange).Split('-').TryCollectInto(&begin, &end)) {
+ if (TryFromString<ui32>(strRange), begin) {
+ end = begin;
+ } else {
+ break; // TODO
+ }
+ }
+ ranges.push_back({begin, end});
+ }
+ sort(ranges.begin(), ranges.end());
+ auto it = ranges.begin();
+ auto current = *(it)++;
+ while (it != ranges.end()) {
+ if (current.second > it->first){
+ current.second = std::max(current.second, it->second);
+ } else {
+ auto* newRange = request->Record.AddRanges();
+ newRange->SetBegin(current.first);
+ newRange->SetEnd(current.second);
+ current = *(it);
+ }
+ it++;
+ }
+ auto* newRange = request->Record.AddRanges();
+ newRange->SetBegin(current.first);
+ newRange->SetEnd(current.second);
+ }
return request;
}
diff --git a/ydb/core/cms/sentinel.cpp b/ydb/core/cms/sentinel.cpp
index 67ee9f914a3..e7ccde90a9a 100644
--- a/ydb/core/cms/sentinel.cpp
+++ b/ydb/core/cms/sentinel.cpp
@@ -476,6 +476,7 @@ public:
}
void PassAway() override {
+ SentinelState->PrevConfigUpdaterState = SentinelState->ConfigUpdaterState;
SentinelState->ConfigUpdaterState.Clear();
TActor::PassAway();
}
@@ -753,6 +754,8 @@ public:
}
void PassAway() override {
+ Info->LastStatusChange = Now();
+ Info->PrevStatusChangerState = Info->StatusChangerState;
Info->StatusChangerState.Reset();
TActor::PassAway();
}
@@ -820,13 +823,24 @@ class TSentinel: public TActorBootstrapped<TSentinel> {
}
};
- struct TUpdaterInfo {
+ struct TUpdaterState {
TActorId Id;
TInstant StartedAt;
bool Delayed;
+ void Clear() {
+ Id = TActorId();
+ StartedAt = TInstant::Zero();
+ Delayed = false;
+ }
+ };
+
+ struct TUpdaterInfo: public TUpdaterState {
+ TUpdaterState PrevState;
+
TUpdaterInfo() {
- Clear();
+ PrevState.Clear();
+ TUpdaterState::Clear();
}
void Start(const TActorId& id, const TInstant& now) {
@@ -836,9 +850,8 @@ class TSentinel: public TActorBootstrapped<TSentinel> {
}
void Clear() {
- Id = TActorId();
- StartedAt = TInstant::Zero();
- Delayed = false;
+ PrevState = *this;
+ TUpdaterState::Clear();
}
};
@@ -1021,41 +1034,99 @@ class TSentinel: public TActorBootstrapped<TSentinel> {
}
void Handle(TEvCms::TEvGetSentinelStateRequest::TPtr& ev) {
+ const auto& reqRecord = ev->Get()->Record;
+
+ auto show = NKikimrCms::TGetSentinelStateRequest::UNHEALTHY;
+
+ if (reqRecord.HasShow()) {
+ show = reqRecord.GetShow();
+ }
+
+ TMap<ui32, ui32> ranges = {{1, 20}};
+
+ if (reqRecord.RangesSize() > 0) {
+ ranges.clear();
+ for (size_t i = 0; i < reqRecord.RangesSize(); i++) {
+ auto range = reqRecord.GetRanges(i);
+ if (range.HasBegin() && range.HasEnd()) {
+ ranges.emplace(range.GetBegin(), range.GetEnd());
+ }
+ }
+ }
+
+ auto checkRanges = [&](ui32 NodeId) {
+ auto next = ranges.upper_bound(NodeId);
+ if (next != ranges.begin()) {
+ --next;
+ return next->second >= NodeId;
+ }
+
+ return false;
+ };
+
+ auto filterByStatus = [](const TPDiskInfo& info, NKikimrCms::TGetSentinelStateRequest::EShow filter) {
+ switch(filter) {
+ case NKikimrCms::TGetSentinelStateRequest::UNHEALTHY:
+ return info.GetState() != NKikimrBlobStorage::TPDiskState::Normal || info.GetStatus() != EPDiskStatus::ACTIVE;
+ case NKikimrCms::TGetSentinelStateRequest::SUSPICIOUS:
+ return info.GetState() != NKikimrBlobStorage::TPDiskState::Normal
+ || info.GetStatus() != EPDiskStatus::ACTIVE
+ || info.StatusChangerState
+ || !info.IsTouched()
+ || !info.IsChangingAllowed();
+ default:
+ return true;
+ }
+ };
+
auto response = MakeHolder<TEvCms::TEvGetSentinelStateResponse>();
auto& record = response->Record;
record.MutableStatus()->SetCode(NKikimrCms::TStatus::OK);
Config.Serialize(*record.MutableSentinelConfig());
+ auto serializeUpdater = [](const auto& updater, auto* out){
+ out->SetActorId(updater.Id.ToString());
+ out->SetStartedAt(updater.StartedAt.ToString());
+ out->SetDelayed(updater.Delayed);
+ };
+
if (SentinelState) {
auto& stateUpdater = *record.MutableStateUpdater();
- stateUpdater.MutableUpdaterInfo()->SetActorId(StateUpdater.Id.ToString());
- stateUpdater.MutableUpdaterInfo()->SetStartedAt(StateUpdater.StartedAt.ToString());
- stateUpdater.MutableUpdaterInfo()->SetDelayed(StateUpdater.Delayed);
+ serializeUpdater(StateUpdater, stateUpdater.MutableUpdaterInfo());
+ serializeUpdater(StateUpdater.PrevState, stateUpdater.MutablePrevUpdaterInfo());
for (const auto& waitNode : SentinelState->StateUpdaterWaitNodes) {
stateUpdater.AddWaitNodes(waitNode);
}
auto& configUpdater = *record.MutableConfigUpdater();
- configUpdater.MutableUpdaterInfo()->SetActorId(ConfigUpdater.Id.ToString());
- configUpdater.MutableUpdaterInfo()->SetStartedAt(ConfigUpdater.StartedAt.ToString());
- configUpdater.MutableUpdaterInfo()->SetDelayed(ConfigUpdater.Delayed);
+ serializeUpdater(ConfigUpdater, configUpdater.MutableUpdaterInfo());
+ serializeUpdater(ConfigUpdater.PrevState, configUpdater.MutablePrevUpdaterInfo());
configUpdater.SetBSCAttempt(SentinelState->ConfigUpdaterState.BSCAttempt);
+ configUpdater.SetPrevBSCAttempt(SentinelState->PrevConfigUpdaterState.BSCAttempt);
configUpdater.SetCMSAttempt(SentinelState->ConfigUpdaterState.CMSAttempt);
+ configUpdater.SetPrevCMSAttempt(SentinelState->PrevConfigUpdaterState.CMSAttempt);
for (const auto& [id, info] : SentinelState->PDisks) {
- auto& entry = *record.AddPDisks();
- entry.MutableId()->SetNodeId(id.NodeId);
- entry.MutableId()->SetDiskId(id.DiskId);
- entry.MutableInfo()->SetState(info->GetState());
- entry.MutableInfo()->SetPrevState(info->GetPrevState());
- entry.MutableInfo()->SetStateCounter(info->GetStateCounter());
- entry.MutableInfo()->SetStatus(info->GetStatus());
- entry.MutableInfo()->SetChangingAllowed(info->IsChangingAllowed());
- entry.MutableInfo()->SetTouched(info->IsTouched());
- if(info->StatusChangerState) {
- entry.MutableInfo()->SetDesiredStatus(info->StatusChangerState->Status);
- entry.MutableInfo()->SetStatusChangeAttempts(info->StatusChangerState->Attempt);
+ if (filterByStatus(*info, show) && checkRanges(id.NodeId)) {
+ auto& entry = *record.AddPDisks();
+ entry.MutableId()->SetNodeId(id.NodeId);
+ entry.MutableId()->SetDiskId(id.DiskId);
+ entry.MutableInfo()->SetState(info->GetState());
+ entry.MutableInfo()->SetPrevState(info->GetPrevState());
+ entry.MutableInfo()->SetStateCounter(info->GetStateCounter());
+ entry.MutableInfo()->SetStatus(info->GetStatus());
+ entry.MutableInfo()->SetChangingAllowed(info->IsChangingAllowed());
+ entry.MutableInfo()->SetTouched(info->IsTouched());
+ entry.MutableInfo()->SetLastStatusChange(info->LastStatusChange.ToString());
+ if (info->StatusChangerState) {
+ entry.MutableInfo()->SetDesiredStatus(info->StatusChangerState->Status);
+ entry.MutableInfo()->SetStatusChangeAttempts(info->StatusChangerState->Attempt);
+ }
+ if (info->PrevStatusChangerState) {
+ entry.MutableInfo()->SetPrevDesiredStatus(info->PrevStatusChangerState->Status);
+ entry.MutableInfo()->SetPrevStatusChangeAttempts(info->PrevStatusChangerState->Attempt);
+ }
}
}
}
diff --git a/ydb/core/cms/sentinel_impl.h b/ydb/core/cms/sentinel_impl.h
index 05423e3ab02..00029ed6169 100644
--- a/ydb/core/cms/sentinel_impl.h
+++ b/ydb/core/cms/sentinel_impl.h
@@ -87,8 +87,11 @@ struct TPDiskInfo
, public TPDiskStatus
{
using TPtr = TIntrusivePtr<TPDiskInfo>;
+
TActorId StatusChanger;
+ TInstant LastStatusChange;
TStatusChangerState::TPtr StatusChangerState;
+ TStatusChangerState::TPtr PrevStatusChangerState;
explicit TPDiskInfo(EPDiskStatus initialStatus, const ui32& defaultStateLimit, const TLimitsMap& stateLimits);
@@ -128,6 +131,7 @@ struct TSentinelState: public TSimpleRefCount<TSentinelState> {
TMap<TNodeId, TNodeInfo> Nodes;
THashSet<ui32> StateUpdaterWaitNodes;
TConfigUpdaterState ConfigUpdaterState;
+ TConfigUpdaterState PrevConfigUpdaterState;
};
class TClusterMap {
diff --git a/ydb/core/cms/ui/index.html b/ydb/core/cms/ui/index.html
index 2c3a0b98e87..f5a26485e27 100644
--- a/ydb/core/cms/ui/index.html
+++ b/ydb/core/cms/ui/index.html
@@ -97,6 +97,21 @@
<table id="sentinel-config"></table>
<table id="sentinel-state-updater"></table>
<table id="sentinel-config-updater"></table>
+ <form autocomplete="off" id="sentinel-switch">
+ <input autocomplete="off" type="radio" id="sentinel-unhealthy" name="sentinel-switch" value="UNHEALTHY" checked/>
+ <label for="sentinel-unhealthy">Unhealthy</label>
+ <input autocomplete="off" type="radio" id="sentinel-suspicious" name="sentinel-switch" value="SUSPICIOUS" />
+ <label for="sentinel-suspicious">Suspicious</label>
+ <input autocomplete="off" type="radio" id="sentinel-all" name="sentinel-switch" value="ALL" />
+ <label for="sentinel-all">All</label>
+ </form>
+ <div>
+ Show nodes:
+ <input autocomplete="off" type="text" id="sentinel-range" name="sentinel-range" required value="1-20"/>
+ <input type="button" id="sentinel-refresh-range" value="Go"/>
+ <div id="sentinel-range-error" class="error"></div>
+ <div id="sentinel-filter-controls"></div>
+ </div>
<table id="sentinel-nodes"></table>
</div>
</div>
diff --git a/ydb/core/cms/ui/nanotable.js b/ydb/core/cms/ui/nanotable.js
index 1d3e3466a99..337e8fb4f85 100644
--- a/ydb/core/cms/ui/nanotable.js
+++ b/ydb/core/cms/ui/nanotable.js
@@ -69,8 +69,9 @@ class ProxyCell {
}
class Table {
- constructor(elem, onCellUpdate) {
+ constructor(elem, onCellUpdate, onInsertColumn) {
this.elem = elem;
+ this.onInsertColumn = onInsertColumn;
this.rows = [];
this.onCellUpdate = onCellUpdate;
}
@@ -78,13 +79,33 @@ class Table {
addRow(columns) {
var cells = [];
for (var column in columns) {
- cells.push(new Cell(columns[column], this.onCellUpdate));
+ var cell = new Cell(columns[column], this.onCellUpdate);
+ if (this.onInsertColumnt !== undefined) {
+ onInsertColumn(cell, column);
+ }
+ cells.push(cell);
}
this.rows.push(cells);
this._drawRow(this.rows.length - 1);
return this.rows[this.rows.length - 1];
}
+ removeRow(rowId) {
+ this.elem.children().eq(rowId).remove();
+ var row = this.rows[rowId];
+ this.rows.splice(rowId, 1);
+ for (var cell of row) {
+ if (cell.isProxy()) {
+ cell.cell.setRowspan(cell.cell.rowspan - 1)
+ }
+ }
+ }
+
+ removeRowByElem(cell) {
+ var index = cell.elem.parent().index();
+ return this.removeRow(index);
+ }
+
insertRow(rowId, columns) {
var cells = [];
var ignoreColspan = 0;
@@ -93,7 +114,11 @@ class Table {
this.rows[rowId] === undefined ||
!this.rows[rowId][column].isProxy()
) {
- cells.push(new Cell(columns[column], this.onCellUpdate));
+ var cell = new Cell(columns[column], this.onCellUpdate);
+ if (this.onInsertColumnt !== undefined) {
+ this.onInsertColumn(cell, column);
+ }
+ cells.push(cell);
} else {
var spanCell = this.at(rowId, column);
if (ignoreColspan === 0) {
diff --git a/ydb/core/cms/ui/sentinel.css b/ydb/core/cms/ui/sentinel.css
index 623644c6416..cf9b7047d80 100644
--- a/ydb/core/cms/ui/sentinel.css
+++ b/ydb/core/cms/ui/sentinel.css
@@ -5,6 +5,20 @@
border: #cdcdcd 1px solid;
}
+.sentinel-checkbox {
+ display: inline-block;
+ padding-right: 8px;
+ padding-top: 4px;
+}
+
+.sentinel-checkbox > label {
+ padding: 0 4px;
+}
+
+#sentinel-state > form {
+ margin-bottom: 0;
+}
+
#sentinel-state th {
background-color: #99bfe6;
font-weight: bold;
diff --git a/ydb/core/cms/ui/sentinel_state.js b/ydb/core/cms/ui/sentinel_state.js
index 36271a9b995..b35021afa8d 100644
--- a/ydb/core/cms/ui/sentinel_state.js
+++ b/ydb/core/cms/ui/sentinel_state.js
@@ -1,18 +1,5 @@
'use strict';
-var CmsSentinelState = {
- fetchInterval: 5000,
- nodes: {},
- pdisks: {},
- config: {},
- stateUpdater: {},
- configUpdater: {},
-};
-
-function id(arg) {
- return { "value": arg === undefined ? "nil" : arg };
-}
-
var TPDiskState = [
"Initial",
"InitialFormatRead",
@@ -35,16 +22,6 @@ TPDiskState[253] = "Timeout";
TPDiskState[254] = "NodeDisconnected";
TPDiskState[255] = "Unknown";
-function state(highlight, arg) {
- var res = {
- "value": arg + ":" + TPDiskState[arg]
- };
- if (highlight == true) {
- res.class = arg == 10 ? "green" : "red"
- }
- return res;
-}
-
const EPDiskStatus = [
"UNKNOWN",
"ACTIVE",
@@ -54,56 +31,6 @@ const EPDiskStatus = [
"TO_BE_REMOVED",
];
-function status(arg) {
- return { "value": arg === undefined ? "nil" : arg + ":" + EPDiskStatus[arg], "class": arg === 1 ? "green" : (arg === undefined ? undefined : "red") };
-}
-
-function bool(arg) {
- return { "value": arg === true ? "+" : "-" };
-}
-
-const PDiskInfoValueMappers = {
- "State": function(arg) { return state(true, arg); },
- "PrevState": function(arg) { return state(false, arg); },
- "StateCounter": id,
- "Status": status,
- "ChangingAllowed": bool,
- "Touched": bool,
- "DesiredStatus": status,
- "StatusChangeAttempts": id,
-}
-
-function nameToSelector(name) {
- return (name.charAt(0).toLowerCase() + name.slice(1)).replace(/([A-Z])/g, "-$1").toLowerCase();
-}
-
-function nameToMember(name) {
- return (name.charAt(0).toLowerCase() + name.slice(1));
-}
-
-function restartAnimation(node) {
- var el = node;
- var newone = el.clone(true);
- el.before(newone);
- el.remove()
-}
-
-function mapPDiskState(cell, key, text, silent = false) {
- if (PDiskInfoValueMappers.hasOwnProperty(key)) {
- var data = PDiskInfoValueMappers[key](text);
- cell.setText(
- data.value,
- silent
- );
- if (data.hasOwnProperty("class")) {
- cell.elem.removeClass("red");
- cell.elem.removeClass("yellow");
- cell.elem.removeClass("green");
- cell.elem.addClass(data.class);
- }
- }
-}
-
const PDiskHeaders = [
"PDiskId",
"State",
@@ -114,184 +41,384 @@ const PDiskHeaders = [
"Touched",
"DesiredStatus",
"StatusChangeAttempts",
+ "PrevDesiredStatus",
+ "PrevStatusChangeAttempts",
+ "LastStatusChange",
];
-function buildPDisksTableHeader(table, width) {
- var headers = ["Node", "PDisk"];
- for (var i = headers.length; i < width; ++i) {
- headers.push("");
+class CmsSentinelState {
+
+ constructor() {
+ this.fetchInterval = 5000;
+ this.nodes = {};
+ this.pdisks = {};
+ this.config = {};
+ this.stateUpdater = {};
+ this.configUpdater = {};
+ this.show = "UNHEALTHY";
+ this.range = "1-20";
+ this.filtered = {};
+ this.filteredSize = 0;
+ this.gen = 0;
+
+ this.initTab();
}
- var row = table.addRow(headers);
- row[0].setHeader(true);
- row[1].setHeader(true);
- table.merge(0, 0, 1, width);
-}
-function buildNodeHeader(table, NodeId) {
- var headers = [NodeId].concat(PDiskHeaders);
- var row = table.addRow(headers);
- row[0].elem.addClass("side");
- for (var i = 1; i < row.length; ++i) {
- row[i].setHeader(true);
+ buildPVHeader(table, header) {
+ var headers = [header, ""];
+ var row = table.addRow(headers);
+ row[0].setHeader(true);
+ table.merge(0, 0, 0, 1);
+ headers = ["Param", "Value"];
+ row = table.addRow(headers);
+ row[0].setHeader(true);
+ row[1].setHeader(true);
+ return row;
}
- return row;
-}
-function buildPDisk(table, header, id, diskData) {
- diskData["PDiskId"] = id;
- var data = [""].concat(PDiskHeaders.map((x) => diskData[x]));
- var row = table.insertRowAfter(header[0], data);
- for (var i = 2; i < row.length; ++i) {
- var key = PDiskHeaders[i - 1];
- mapPDiskState(row[i], key, diskData[key], true)
+ addPVEntry(table, header, key, value) {
+ var data = [key, value];
+ var row = table.insertRowAfter(header, data);
+ return row;
}
- return row;
-}
-function updatePDisk(table, row, id, data, prevData) {
- for (var i = 2; i < row.length; ++i) {
- var key = PDiskHeaders[i - 1];
- if (data[key] !== prevData[key]) {
- mapPDiskState(row[i], key, data[key])
+ updatePVEntry(table, row, value, prevValue) {
+ if(value !== prevValue) {
+ row[1].setText(value);
}
}
-}
-function buildPVHeader(table, header) {
- var headers = [header, ""];
- var row = table.addRow(headers);
- row[0].setHeader(true);
- table.merge(0, 0, 0, 1);
- headers = ["Param", "Value"];
- row = table.addRow(headers);
- row[0].setHeader(true);
- row[1].setHeader(true);
- return row;
-}
+ renderPVEntry(entry, newData) {
+ var table = entry.table;
+ var headers = entry.header;
+ var data = entry.data;
+ for (var entry in newData) {
+ if (!data.hasOwnProperty(entry)) {
+ var row = this.addPVEntry(table, headers[0], entry, newData[entry]);
+ data[entry] = {
+ row: row,
+ data: newData[entry],
+ };
+ } else {
+ this.updatePVEntry(
+ table,
+ data[entry].row,
+ newData[entry],
+ data[entry].data);
+ data[entry].data = newData[entry];
+ }
+ }
+ }
-function addPVEntry(table, header, key, value) {
- var data = [key, value];
- var row = table.insertRowAfter(header, data);
- return row;
-}
+ id(arg) {
+ return { "value": arg === undefined ? "nil" : arg };
+ }
-function updatePVEntry(table, row, value, prevValue) {
- if(value !== prevValue) {
- row[1].setText(value);
+ state(highlight, arg) {
+ var res = {
+ "value": arg + ":" + TPDiskState[arg]
+ };
+ if (highlight == true) {
+ res.class = arg == 10 ? "green" : "red"
+ }
+ return res;
}
-}
-function renderPVEntry(entry, newData) {
- var table = entry.table;
- var headers = entry.header;
- var data = entry.data;
- for (var entry in newData) {
- if (!data.hasOwnProperty(entry)) {
- var row = addPVEntry(table, headers[0], entry, newData[entry]);
- data[entry] = {
- row: row,
- data: newData[entry],
- };
- } else {
- updatePVEntry(
- table,
- data[entry].row,
- newData[entry],
- data[entry].data);
- data[entry].data = newData[entry];
+ status(arg) {
+ return { "value": arg === undefined ? "nil" : arg + ":" + EPDiskStatus[arg], "class": arg === 1 ? "green" : (arg === undefined ? undefined : "red") };
+ }
+
+ bool(arg) {
+ return { "value": arg === true ? "+" : "-" };
+ }
+
+ getPDiskInfoValueMappers() {
+ return {
+ "State": function(arg) { return this.state(true, arg); }.bind(this),
+ "PrevState": function(arg) { return this.state(false, arg); }.bind(this),
+ "StateCounter": this.id.bind(this),
+ "Status": this.status.bind(this),
+ "ChangingAllowed": this.bool.bind(this),
+ "Touched": this.bool.bind(this),
+ "DesiredStatus": this.status.bind(this),
+ "StatusChangeAttempts": this.id.bind(this),
+ "PrevDesiredStatus": this.id.bind(this),
+ "PrevStatusChangeAttempts": this.id.bind(this),
+ "LastStatusChange": this.id.bind(this),
+ };
+ }
+
+ nameToSelector(name) {
+ return (name.charAt(0).toLowerCase() + name.slice(1)).replace(/([A-Z])/g, "-$1").toLowerCase();
+ }
+
+ nameToMember(name) {
+ return (name.charAt(0).toLowerCase() + name.slice(1));
+ }
+
+ restartAnimation(node) {
+ var el = node;
+ var newone = el.clone(true);
+ el.before(newone);
+ el.remove()
+ }
+
+ mapPDiskState(cell, key, text, silent = false) {
+ if (this.getPDiskInfoValueMappers().hasOwnProperty(key)) {
+ var data = this.getPDiskInfoValueMappers()[key](text);
+ cell.setText(
+ data.value,
+ silent
+ );
+ if (data.hasOwnProperty("class")) {
+ cell.elem.removeClass("red");
+ cell.elem.removeClass("yellow");
+ cell.elem.removeClass("green");
+ cell.elem.addClass(data.class);
+ }
}
}
-}
-function renderPDisksTable(data) {
- var table = CmsSentinelState.pdisksTable;
-
- for (var ipdisk in data["PDisks"]) {
- var pdisk = data["PDisks"][ipdisk];
- var NodeId = pdisk["Id"]["NodeId"];
- var PDiskId = pdisk["Id"]["DiskId"];
- var PDiskInfo = pdisk["Info"];
- if (!CmsSentinelState.nodes.hasOwnProperty(NodeId)) {
- var row = buildNodeHeader(table, NodeId);
- CmsSentinelState.nodes[NodeId] = {
- header: row,
- pdisks: {}
- };
+ buildPDisksTableHeader(table, width) {
+ var headers = ["Node", "PDisk"];
+ for (var i = headers.length; i < width; ++i) {
+ headers.push("");
}
- if (!CmsSentinelState.nodes[NodeId].pdisks.hasOwnProperty(PDiskId)) {
- var header = CmsSentinelState.nodes[NodeId].header;
- var row = buildPDisk(table, header, PDiskId, PDiskInfo);
+ var row = table.addRow(headers);
+ row[0].setHeader(true);
+ row[1].setHeader(true);
+ table.merge(0, 0, 1, width);
+ }
+
+ columnFilter(el) {
+ if (this.filtered.hasOwnProperty(el) && this.filtered[el]) {
+ return false;
+ }
+ return true;
+ }
+
+ buildNodeHeader(table, NodeId) {
+ var headers = [NodeId].concat(PDiskHeaders).filter(this.columnFilter.bind(this));
+ var row = table.addRow(headers);
+ row[0].elem.addClass("side");
+ for (var i = 1; i < row.length; ++i) {
+ row[i].setHeader(true);
+ }
+ return row;
+ }
+
+ buildPDisk(table, header, id, diskData) {
+ diskData["PDiskId"] = id;
+ var deletionMarker = "DELETED";
+ var data = [""].concat(PDiskHeaders.map((x) => this.columnFilter(x) ? diskData[x] : deletionMarker)).filter((x) => x !== deletionMarker);
+ var row = table.insertRowAfter(header[0], data);
+ var filteredHeaders = PDiskHeaders.filter(this.columnFilter.bind(this));
+ for (var i = 2; i < row.length; ++i) {
+ var key = filteredHeaders[i - 1];
+ this.mapPDiskState(row[i], key, diskData[key], true);
+ }
+ return row;
+ }
- table.mergeCells(header[0], row[0]);
+ updatePDisk(table, row, id, data, prevData) {
+ for (var i = 2; i < row.length; ++i) {
+ var key = PDiskHeaders[i - 1];
+ if (data[key] !== prevData[key]) {
+ this.mapPDiskState(row[i], key, data[key]);
+ }
+ }
+ }
- CmsSentinelState.nodes[NodeId].pdisks[PDiskId] = {
- row: row,
- data: PDiskInfo,
- };
+ removeOutdated() {
+ for (var node in this.nodes) {
+ for (var pdisk in this.nodes[node].pdisks) {
+ if (this.nodes[node].pdisks[pdisk].gen != this.gen) {
+ this.pdisksTable.removeRowByElem(this.nodes[node].pdisks[pdisk].row[1]); // first because zero is a proxy
+ delete this.nodes[node].pdisks[pdisk];
+ }
+ }
+ if (Object.keys(this.nodes[node].pdisks).length === 0) {
+ this.pdisksTable.removeRowByElem(this.nodes[node].header[0]);
+ delete this.nodes[node];
+ }
+ }
+ }
+
+ renderPDisksTable(data) {
+ var table = this.pdisksTable;
+
+ this.gen++;
+ var currentGen = this.gen;
+
+ for (var ipdisk in data["PDisks"]) {
+ var pdisk = data["PDisks"][ipdisk];
+ var NodeId = pdisk["Id"]["NodeId"];
+ var PDiskId = pdisk["Id"]["DiskId"];
+ var PDiskInfo = pdisk["Info"];
+ if (!this.nodes.hasOwnProperty(NodeId)) {
+ var row = this.buildNodeHeader(table, NodeId);
+ this.nodes[NodeId] = {
+ header: row,
+ pdisks: {}
+ };
+ }
+ if (!this.nodes[NodeId].pdisks.hasOwnProperty(PDiskId)) {
+ var header = this.nodes[NodeId].header;
+ var row = this.buildPDisk(table, header, PDiskId, PDiskInfo);
+
+ table.mergeCells(header[0], row[0]);
+
+ this.nodes[NodeId].pdisks[PDiskId] = {
+ row: row,
+ data: PDiskInfo,
+ gen: currentGen,
+ };
+ } else {
+ var prevState = this.nodes[NodeId].pdisks[PDiskId];
+ this.updatePDisk(table, prevState.row, PDiskId, PDiskInfo, prevState.data);
+ prevState.data = PDiskInfo;
+ prevState.gen = currentGen;
+ }
+ }
+
+ this.removeOutdated();
+ }
+
+ onThisLoaded(data) {
+ if (data?.Status?.Code === "OK") {
+ $("#sentinel-error").empty();
+
+ this.renderPDisksTable(data);
+
+ this.renderPVEntry(this.config, data["SentinelConfig"]);
+
+ var flattenStateUpdaterResp = data["StateUpdater"]["UpdaterInfo"];
+ flattenStateUpdaterResp["WaitNodes"] = data["StateUpdater"]["WaitNodes"];
+ for (var key in data["StateUpdater"]["PrevUpdaterInfo"]) {
+ flattenStateUpdaterResp["Prev" + key] = data["StateUpdater"]["PrevUpdaterInfo"][key];
+ }
+ this.renderPVEntry(this.stateUpdater, flattenStateUpdaterResp);
+
+ var flattenConfigUpdaterResp = data["ConfigUpdater"]["UpdaterInfo"];
+ flattenConfigUpdaterResp["BSCAttempt"] = data["ConfigUpdater"]["BSCAttempt"];
+ flattenConfigUpdaterResp["CMSAttempt"] = data["ConfigUpdater"]["CMSAttempt"];
+ flattenConfigUpdaterResp["PrevBSCAttempt"] = data["ConfigUpdater"]["PrevBSCAttempt"];
+ flattenConfigUpdaterResp["PrevCMSAttempt"] = data["ConfigUpdater"]["PrevCMSAttempt"];
+ for (var key in data["StateUpdater"]["PrevUpdaterInfo"]) {
+ flattenConfigUpdaterResp["Prev" + key] = data["ConfigUpdater"]["PrevUpdaterInfo"][key];
+ }
+ this.renderPVEntry(this.configUpdater, flattenConfigUpdaterResp);
} else {
- var prevState = CmsSentinelState.nodes[NodeId].pdisks[PDiskId];
- updatePDisk(table, prevState.row, PDiskId, PDiskInfo, prevState.data);
- prevState.data = PDiskInfo;
+ $("#sentinel-error").text("Error while updating state");
}
+ setTimeout(this.loadThis.bind(this), this.fetchInterval);
+ this.restartAnimation($("#sentinel-anim"));
}
-}
-function onCmsSentinelStateLoaded(data) {
- if (data?.Status?.Code === "OK") {
- $("#sentinel-error").empty();
+ loadThis() {
+ var show = $('input[name="sentinel-switch"]:checked').val();
- renderPDisksTable(data);
+ if (show != this.show) {
+ this.cleanup();
+ }
- renderPVEntry(CmsSentinelState.config, data["SentinelConfig"]);
+ this.show = show;
+ var url = 'cms/api/json/sentinel?show=' + this.show;
+ if (this.range != "") {
+ url = url + "&range=" + this.range;
+ }
+ $.get(url).done(this.onThisLoaded.bind(this));
+ }
- var flattenStateUpdaterResp = data["StateUpdater"]["UpdaterInfo"];
- flattenStateUpdaterResp["WaitNodes"] = data["StateUpdater"]["WaitNodes"];
- renderPVEntry(CmsSentinelState.stateUpdater, flattenStateUpdaterResp);
+ onCellUpdate(cell) {
+ cell.elem.addClass("highlight");
+ var el = cell.elem;
+ var newone = el.clone(true);
+ el.before(newone);
+ el.remove();
+ cell.elem = newone;
+ }
- var flattenConfigUpdaterResp = data["ConfigUpdater"]["UpdaterInfo"];
- flattenConfigUpdaterResp["BSCAttempt"] = data["ConfigUpdater"]["BSCAttempt"];
- flattenConfigUpdaterResp["CMSAttempt"] = data["ConfigUpdater"]["CMSAttempt"];
- renderPVEntry(CmsSentinelState.configUpdater, flattenConfigUpdaterResp);
- } else {
- $("#sentinel-error").text("Error while updating state");
+ onInsertColumn(cell, columnId) {
+ cell.onUpdate = cell.onUpdate;
}
- setTimeout(loadCmsSentinelState, CmsSentinelState.fetchInterval);
- restartAnimation($("#sentinel-anim"));
-}
-function loadCmsSentinelState() {
- var url = 'cms/api/json/sentinel';
- $.get(url).done(onCmsSentinelStateLoaded);
-}
+ preparePVTable(name) {
+ var table = new Table($("#sentinel-" + this.nameToSelector(name)), this.onCellUpdate);
+ var header = this.buildPVHeader(table, name);
+ this[this.nameToMember(name)] = {
+ header: header,
+ table: table,
+ data: {},
+ };
+ }
-function onCellUpdate(cell) {
- cell.elem.addClass("highlight");
- var el = cell.elem;
- var newone = el.clone(true);
- el.before(newone);
- el.remove();
- cell.elem = newone;
-}
+ refreshRange() {
+ var value = $("#sentinel-range").val();
+ const re = /^(?:(?:\d+|(?:\d+-\d+)),)*(?:\d+|(?:\d+-\d+))$/;
+ if (re.test(value)) {
+ $("#sentinel-range-error").empty();
+ this.range = value;
+ this.cleanup();
+ } else {
+ $("#sentinel-range-error").text("Invalid range");
+ }
+ }
-function preparePVTable(name) {
- var table = new Table($("#sentinel-" + nameToSelector(name)), onCellUpdate);
- var header = buildPVHeader(table, name);
- CmsSentinelState[nameToMember(name)] = {
- header: header,
- table: table,
- data: {},
- };
-}
+ addCheckbox(elem, name) {
+ var cb = $('<input />', { type: 'checkbox', id: 'cb-' + name, value: name, checked: 'checked' });
+
+ cb.change(function() {
+ if(cb[0].checked) {
+ this.filtered[name] = false;
+ this.filteredSize--;
+ } else {
+ this.filtered[name] = true;
+ this.filteredSize++;
+ }
+ this.cleanup();
+ }.bind(this)).appendTo(elem);
+ $('<label />', { 'for': 'cb-' + name, text: name, }).appendTo(elem);
+ }
-function initCmsSentinelTab() {
- $("#sentinel-anim").addClass("anim");
+ addFilterCheckboxes() {
+ var elem = $("#sentinel-filter-controls");
+ for (var column of PDiskHeaders) {
+ this.filtered[column] = false;
+ if (column !== "PDiskId") {
+ var div = $('<div />', { class: 'sentinel-checkbox' })
+ this.addCheckbox(div, column)
+ div.appendTo(elem);
+ }
+ }
+ }
- for (var name of ["Config", "StateUpdater", "ConfigUpdater"]) {
- preparePVTable(name);
+ cleanup() {
+ this.nodes = {};
+ this.pdisks = {};
+ this.pdisksTable?.elem.empty();
+ this.pdisksTable = new Table($("#sentinel-nodes"), this.onCellUpdate, this.onInsertColumn);
+ this.buildPDisksTableHeader(this.pdisksTable, PDiskHeaders.length + 1 - this.filteredSize);
}
- CmsSentinelState.pdisksTable = new Table($("#sentinel-nodes"), onCellUpdate);
- buildPDisksTableHeader(CmsSentinelState.pdisksTable, PDiskHeaders.length + 1);
+ initTab() {
+ $("#sentinel-anim").addClass("anim");
+ $("#sentinel-refresh-range").click(this.refreshRange);
+
+ for (var name of ["Config", "StateUpdater", "ConfigUpdater"]) {
+ this.preparePVTable(name);
+ }
+
+ this.addFilterCheckboxes();
+
+ this.cleanup();
- loadCmsSentinelState();
+ this.loadThis();
+ }
+}
+
+var cmsSentinelState;
+
+function initCmsSentinelTab() {
+ cmsSentinelState = new CmsSentinelState();
}
diff --git a/ydb/core/protos/cms.proto b/ydb/core/protos/cms.proto
index 181a8c51f94..85168eab00a 100644
--- a/ydb/core/protos/cms.proto
+++ b/ydb/core/protos/cms.proto
@@ -590,7 +590,19 @@ message TGetLogTailResponse {
repeated TLogRecord LogRecords = 2;
}
+message TFilterRange {
+ optional uint32 Begin = 1;
+ optional uint32 End = 2;
+}
+
message TGetSentinelStateRequest {
+ enum EShow {
+ UNHEALTHY = 1;
+ SUSPICIOUS = 2;
+ ALL = 3;
+ }
+ optional EShow Show = 1;
+ repeated TFilterRange Ranges = 2;
}
message TPDiskInfo {
@@ -602,7 +614,9 @@ message TPDiskInfo {
optional bool Touched = 6;
optional uint32 DesiredStatus = 7;
optional uint32 StatusChangeAttempts = 8;
- optional uint64 LastStatusChange = 9;
+ optional uint32 PrevDesiredStatus = 9;
+ optional uint32 PrevStatusChangeAttempts = 10;
+ optional string LastStatusChange = 11;
}
message TPDisk {
@@ -618,13 +632,17 @@ message TUpdaterInfo {
message TStateUpdaterState {
optional TUpdaterInfo UpdaterInfo = 1;
- repeated uint32 WaitNodes = 2;
+ optional TUpdaterInfo PrevUpdaterInfo = 2;
+ repeated uint32 WaitNodes = 3;
}
message TConfigUpdaterState {
optional TUpdaterInfo UpdaterInfo = 1;
- optional uint32 BSCAttempt = 2;
- optional uint32 CMSAttempt = 3;
+ optional TUpdaterInfo PrevUpdaterInfo = 2;
+ optional uint32 BSCAttempt = 3;
+ optional uint32 PrevBSCAttempt = 4;
+ optional uint32 CMSAttempt = 5;
+ optional uint32 PrevCMSAttempt = 6;
}
message TGetSentinelStateResponse {