diff options
author | innokentii <innokentii@yandex-team.com> | 2022-11-21 16:03:15 +0300 |
---|---|---|
committer | innokentii <innokentii@yandex-team.com> | 2022-11-21 16:03:15 +0300 |
commit | c6fae4ac291a4ef63dcc0439fd6e6816937efbc5 (patch) | |
tree | b0b27f6f7a815b37d2438f70576d808442a941c6 | |
parent | bb9fc75c7a269a63ec9966399916a0cdb05b1353 (diff) | |
download | ydb-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.h | 43 | ||||
-rw-r--r-- | ydb/core/cms/sentinel.cpp | 117 | ||||
-rw-r--r-- | ydb/core/cms/sentinel_impl.h | 4 | ||||
-rw-r--r-- | ydb/core/cms/ui/index.html | 15 | ||||
-rw-r--r-- | ydb/core/cms/ui/nanotable.js | 31 | ||||
-rw-r--r-- | ydb/core/cms/ui/sentinel.css | 14 | ||||
-rw-r--r-- | ydb/core/cms/ui/sentinel_state.js | 557 | ||||
-rw-r--r-- | ydb/core/protos/cms.proto | 26 |
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 { |