mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-06-06 03:37:29 +02:00
Merge branch 'main' into release/v1.5
This commit is contained in:
@@ -238,6 +238,9 @@ func buildFilterPresets(query url.Values) map[string]any {
|
|||||||
if query.Get("cluster") != "" {
|
if query.Get("cluster") != "" {
|
||||||
filterPresets["cluster"] = query.Get("cluster")
|
filterPresets["cluster"] = query.Get("cluster")
|
||||||
}
|
}
|
||||||
|
if query.Get("subCluster") != "" {
|
||||||
|
filterPresets["subCluster"] = query.Get("subCluster")
|
||||||
|
}
|
||||||
if query.Get("partition") != "" {
|
if query.Get("partition") != "" {
|
||||||
filterPresets["partition"] = query.Get("partition")
|
filterPresets["partition"] = query.Get("partition")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ type APIQueryResponse struct {
|
|||||||
// - Type + TypeIds: First level of hierarchy (e.g., "cpu" + ["0", "1", "2"])
|
// - Type + TypeIds: First level of hierarchy (e.g., "cpu" + ["0", "1", "2"])
|
||||||
// - SubType + SubTypeIds: Second level of hierarchy (e.g., "core" + ["0", "1"])
|
// - SubType + SubTypeIds: Second level of hierarchy (e.g., "core" + ["0", "1"])
|
||||||
//
|
//
|
||||||
// If Aggregate is true, data from multiple type/subtype IDs will be aggregated according
|
// If Aggregate is true, data from multiple type/stype IDs will be aggregated according
|
||||||
// to the metric's aggregation strategy. Otherwise, separate results are returned for each combination.
|
// to the metric's aggregation strategy. Otherwise, separate results are returned for each combination.
|
||||||
type APIQuery struct {
|
type APIQuery struct {
|
||||||
Type *string `json:"type,omitempty"`
|
Type *string `json:"type,omitempty"`
|
||||||
@@ -174,13 +174,13 @@ func (data *APIMetricData) PadDataWithNull(ms *MemoryStore, from, to int64, metr
|
|||||||
// This is the primary API for retrieving metric data from the memory store. It supports:
|
// This is the primary API for retrieving metric data from the memory store. It supports:
|
||||||
// - Individual queries via req.Queries
|
// - Individual queries via req.Queries
|
||||||
// - Batch queries for all nodes via req.ForAllNodes
|
// - Batch queries for all nodes via req.ForAllNodes
|
||||||
// - Hierarchical selector construction (cluster → host → type → subtype)
|
// - Hierarchical selector construction (cluster → host → type → stype)
|
||||||
// - Optional statistics computation (avg, min, max)
|
// - Optional statistics computation (avg, min, max)
|
||||||
// - Optional data scaling
|
// - Optional data scaling
|
||||||
// - Optional data padding with NaN values
|
// - Optional data padding with NaN values
|
||||||
//
|
//
|
||||||
// The function constructs selectors based on the query parameters and calls MemoryStore.Read()
|
// The function constructs selectors based on the query parameters and calls MemoryStore.Read()
|
||||||
// for each selector. If a query specifies Aggregate=false with multiple type/subtype IDs,
|
// for each selector. If a query specifies Aggregate=false with multiple type/stype IDs,
|
||||||
// separate results are returned for each combination.
|
// separate results are returned for each combination.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
// This file implements ingestion of InfluxDB line-protocol metric data received
|
// This file implements ingestion of InfluxDB line-protocol metric data received
|
||||||
// over NATS. Each line encodes one metric sample with the following structure:
|
// over NATS. Each line encodes one metric sample with the following structure:
|
||||||
//
|
//
|
||||||
// <measurement>[,cluster=<c>][,hostname=<h>][,type=<t>][,type-id=<id>][,subtype=<s>][,stype-id=<id>] value=<v> [<timestamp>]
|
// <measurement>[,cluster=<c>][,hostname=<h>][,type=<t>][,type-id=<id>][,stype=<s>][,stype-id=<id>] value=<v> [<timestamp>]
|
||||||
//
|
//
|
||||||
// The measurement name identifies the metric (e.g. "cpu_load"). Tags provide
|
// The measurement name identifies the metric (e.g. "cpu_load"). Tags provide
|
||||||
// routing information (cluster, host) and optional sub-device selectors (type,
|
// routing information (cluster, host) and optional sub-device selectors (type,
|
||||||
// subtype). Only one field is expected per line: "value".
|
// stype). Only one field is expected per line: "value".
|
||||||
//
|
//
|
||||||
// After decoding, each sample is:
|
// After decoding, each sample is:
|
||||||
// 1. Written to the in-memory store via ms.WriteToLevel.
|
// 1. Written to the in-memory store via ms.WriteToLevel.
|
||||||
@@ -103,7 +103,7 @@ func ReceiveNats(ms *MemoryStore,
|
|||||||
// reorder prepends prefix to buf in-place when buf has enough spare capacity,
|
// reorder prepends prefix to buf in-place when buf has enough spare capacity,
|
||||||
// avoiding an allocation. Falls back to a regular append otherwise.
|
// avoiding an allocation. Falls back to a regular append otherwise.
|
||||||
//
|
//
|
||||||
// It is used to assemble the "type<type-id>" and "subtype<stype-id>" selector
|
// It is used to assemble the "type<type-id>" and "stype<stype-id>" selector
|
||||||
// strings when the type tag arrives before the type-id tag in the line, so the
|
// strings when the type tag arrives before the type-id tag in the line, so the
|
||||||
// two byte slices need to be concatenated in tag-declaration order regardless
|
// two byte slices need to be concatenated in tag-declaration order regardless
|
||||||
// of wire order.
|
// of wire order.
|
||||||
@@ -145,7 +145,7 @@ type decodeState struct {
|
|||||||
// current line. Reset at the start of each line's tag-decode loop.
|
// current line. Reset at the start of each line's tag-decode loop.
|
||||||
typeBuf []byte
|
typeBuf []byte
|
||||||
|
|
||||||
// subTypeBuf accumulates the concatenated "subtype"+"stype-id" tag value.
|
// subTypeBuf accumulates the concatenated "stype"+"stype-id" tag value.
|
||||||
// Reset at the start of each line's tag-decode loop.
|
// Reset at the start of each line's tag-decode loop.
|
||||||
subTypeBuf []byte
|
subTypeBuf []byte
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ var decodeStatePool = sync.Pool{
|
|||||||
// - The Level pointer (host-level node in the metric tree) is cached across
|
// - The Level pointer (host-level node in the metric tree) is cached across
|
||||||
// consecutive lines that share the same cluster+host pair to avoid
|
// consecutive lines that share the same cluster+host pair to avoid
|
||||||
// repeated lock acquisitions on the root and cluster levels.
|
// repeated lock acquisitions on the root and cluster levels.
|
||||||
// - []byte→string conversions for type/subtype selectors are cached via
|
// - []byte→string conversions for type/stype selectors are cached via
|
||||||
// prevType*/prevSubType* fields because batches typically repeat the same
|
// prevType*/prevSubType* fields because batches typically repeat the same
|
||||||
// sub-device identifiers.
|
// sub-device identifiers.
|
||||||
// - Timestamp parsing tries Second precision first; if that fails it retries
|
// - Timestamp parsing tries Second precision first; if that fails it retries
|
||||||
@@ -269,8 +269,8 @@ func DecodeLine(dec *lineprotocol.Decoder,
|
|||||||
}
|
}
|
||||||
case "type-id":
|
case "type-id":
|
||||||
st.typeBuf = append(st.typeBuf, val...)
|
st.typeBuf = append(st.typeBuf, val...)
|
||||||
case "subtype":
|
case "stype":
|
||||||
// We cannot be sure that the "subtype" tag comes before the "stype-id" tag:
|
// We cannot be sure that the "stype" tag comes before the "stype-id" tag:
|
||||||
if len(st.subTypeBuf) == 0 {
|
if len(st.subTypeBuf) == 0 {
|
||||||
st.subTypeBuf = append(st.subTypeBuf, val...)
|
st.subTypeBuf = append(st.subTypeBuf, val...)
|
||||||
} else {
|
} else {
|
||||||
@@ -291,7 +291,7 @@ func DecodeLine(dec *lineprotocol.Decoder,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// subtypes: cache []byte→string conversions; messages in a batch typically
|
// subtypes: cache []byte→string conversions; messages in a batch typically
|
||||||
// share the same type/subtype so the hit rate is very high.
|
// share the same type/stype so the hit rate is very high.
|
||||||
st.selector = st.selector[:0]
|
st.selector = st.selector[:0]
|
||||||
if len(st.typeBuf) > 0 {
|
if len(st.typeBuf) > 0 {
|
||||||
if !bytes.Equal(st.typeBuf, st.prevTypeBytes) {
|
if !bytes.Equal(st.typeBuf, st.prevTypeBytes) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
userMatch: "contains",
|
userMatch: "contains",
|
||||||
// Filter Modals
|
// Filter Modals
|
||||||
cluster: null,
|
cluster: null,
|
||||||
|
subCluster: null,
|
||||||
partition: null,
|
partition: null,
|
||||||
states: allJobStates,
|
states: allJobStates,
|
||||||
shared: "",
|
shared: "",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
user: filterPresets?.user || "",
|
user: filterPresets?.user || "",
|
||||||
userMatch: filterPresets?.userMatch || "contains",
|
userMatch: filterPresets?.userMatch || "contains",
|
||||||
cluster: filterPresets?.cluster || null,
|
cluster: filterPresets?.cluster || null,
|
||||||
|
subCluster: filterPresets?.subCluster || null,
|
||||||
partition: filterPresets?.partition || null,
|
partition: filterPresets?.partition || null,
|
||||||
states:
|
states:
|
||||||
filterPresets?.states || filterPresets?.state
|
filterPresets?.states || filterPresets?.state
|
||||||
@@ -158,6 +160,7 @@
|
|||||||
if (filters.dbId.length != 0)
|
if (filters.dbId.length != 0)
|
||||||
items.push({ dbId: filters.dbId });
|
items.push({ dbId: filters.dbId });
|
||||||
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
||||||
|
if (filters.subCluster) items.push({ subCluster: { eq: filters.subCluster } });
|
||||||
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
||||||
if (filters.states.length != allJobStates?.length)
|
if (filters.states.length != allJobStates?.length)
|
||||||
items.push({ state: filters.states });
|
items.push({ state: filters.states });
|
||||||
@@ -267,6 +270,7 @@
|
|||||||
opts.push(`userMatch=${filters.userMatch}`);
|
opts.push(`userMatch=${filters.userMatch}`);
|
||||||
// Filter Modals
|
// Filter Modals
|
||||||
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
||||||
|
if (filters.subCluster) opts.push(`subCluster=${filters.subCluster}`);
|
||||||
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
||||||
if (filters.states.length != allJobStates?.length)
|
if (filters.states.length != allJobStates?.length)
|
||||||
for (let state of filters.states) opts.push(`state=${state}`);
|
for (let state of filters.states) opts.push(`state=${state}`);
|
||||||
@@ -346,7 +350,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<DropdownItem header>Manage Filters</DropdownItem>
|
<DropdownItem header>Manage Filters</DropdownItem>
|
||||||
<DropdownItem onclick={() => (isClusterOpen = true)}>
|
<DropdownItem onclick={() => (isClusterOpen = true)}>
|
||||||
<Icon name="cpu" /> Cluster/Partition
|
<Icon name="cpu" /> Cluster/SubCluster/Partition
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem onclick={() => (isJobStatesOpen = true)}>
|
<DropdownItem onclick={() => (isJobStatesOpen = true)}>
|
||||||
<Icon name="gear-fill" /> Job States
|
<Icon name="gear-fill" /> Job States
|
||||||
@@ -440,6 +444,9 @@
|
|||||||
{#if filters.cluster}
|
{#if filters.cluster}
|
||||||
<Info icon="cpu" onclick={() => (isClusterOpen = true)}>
|
<Info icon="cpu" onclick={() => (isClusterOpen = true)}>
|
||||||
{filters.cluster}
|
{filters.cluster}
|
||||||
|
{#if filters.subCluster}
|
||||||
|
[{filters.subCluster}]
|
||||||
|
{/if}
|
||||||
{#if filters.partition}
|
{#if filters.partition}
|
||||||
({filters.partition})
|
({filters.partition})
|
||||||
{/if}
|
{/if}
|
||||||
@@ -603,6 +610,7 @@
|
|||||||
bind:isOpen={isClusterOpen}
|
bind:isOpen={isClusterOpen}
|
||||||
presetCluster={filters.cluster}
|
presetCluster={filters.cluster}
|
||||||
presetPartition={filters.partition}
|
presetPartition={filters.partition}
|
||||||
|
presetSubCluster={filters.subCluster}
|
||||||
{disableClusterSelection}
|
{disableClusterSelection}
|
||||||
setFilter={(filter) => updateFilters(filter)}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<!--
|
<!--
|
||||||
@component Filter sub-component for selecting cluster and subCluster
|
@component Filter sub-component for selecting cluster, partition and subCluster
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
|
- `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
|
||||||
- `presetCluster String?`: The latest selected cluster [Default: ""]
|
- `presetCluster String?`: The latest selected cluster [Default: ""]
|
||||||
- `presetPartition String?`: The latest selected partition [Default: ""]
|
- `presetPartition String?`: The latest selected partition [Default: ""]
|
||||||
|
- `presetSubCluster String?`: The latest selected subCluster [Default: ""]
|
||||||
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
|
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
|
||||||
- `setFilter Func`: The callback function to apply current filter selection
|
- `setFilter Func`: The callback function to apply current filter selection
|
||||||
-->
|
-->
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
isOpen = $bindable(false),
|
isOpen = $bindable(false),
|
||||||
presetCluster = "",
|
presetCluster = "",
|
||||||
presetPartition = "",
|
presetPartition = "",
|
||||||
|
presetSubCluster = "",
|
||||||
disableClusterSelection = false,
|
disableClusterSelection = false,
|
||||||
setFilter
|
setFilter
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -36,10 +38,11 @@
|
|||||||
const clusterInfos = $derived($initialized ? getContext("clusters") : null);
|
const clusterInfos = $derived($initialized ? getContext("clusters") : null);
|
||||||
let pendingCluster = $derived(presetCluster);
|
let pendingCluster = $derived(presetCluster);
|
||||||
let pendingPartition = $derived(presetPartition);
|
let pendingPartition = $derived(presetPartition);
|
||||||
|
let pendingSubCluster = $derived(presetSubCluster);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader>
|
<ModalHeader>Select Cluster, SubCluster & Partition</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if $initialized}
|
{#if $initialized}
|
||||||
<h4>Cluster</h4>
|
<h4>Cluster</h4>
|
||||||
@@ -51,7 +54,7 @@
|
|||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == null}
|
active={pendingCluster == null}
|
||||||
onclick={() => ((pendingCluster = null), (pendingPartition = null))}
|
onclick={() => ((pendingCluster = null), (pendingPartition = null), (pendingSubCluster = null))}
|
||||||
>
|
>
|
||||||
Any Cluster
|
Any Cluster
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
@@ -60,7 +63,7 @@
|
|||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == cluster.name}
|
active={pendingCluster == cluster.name}
|
||||||
onclick={() => (
|
onclick={() => (
|
||||||
(pendingCluster = cluster.name), (pendingPartition = null)
|
(pendingCluster = cluster.name), (pendingPartition = null), (pendingSubCluster = null)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{cluster.name}
|
{cluster.name}
|
||||||
@@ -71,7 +74,27 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if $initialized && pendingCluster != null}
|
{#if $initialized && pendingCluster != null}
|
||||||
<br />
|
<br />
|
||||||
<h4>Partiton</h4>
|
<h4>SubCluster</h4>
|
||||||
|
<ListGroup>
|
||||||
|
<ListGroupItem
|
||||||
|
active={pendingSubCluster == null}
|
||||||
|
onclick={() => (pendingSubCluster = null)}
|
||||||
|
>
|
||||||
|
Any SubCluster
|
||||||
|
</ListGroupItem>
|
||||||
|
{#each clusterInfos?.find((c) => c.name == pendingCluster)?.subClusters as subCluster}
|
||||||
|
<ListGroupItem
|
||||||
|
active={pendingSubCluster == subCluster.name}
|
||||||
|
onclick={() => (pendingSubCluster = subCluster.name)}
|
||||||
|
>
|
||||||
|
{subCluster.name}
|
||||||
|
</ListGroupItem>
|
||||||
|
{/each}
|
||||||
|
</ListGroup>
|
||||||
|
{/if}
|
||||||
|
{#if $initialized && pendingCluster != null}
|
||||||
|
<br />
|
||||||
|
<h4>Partition</h4>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == null}
|
active={pendingPartition == null}
|
||||||
@@ -95,7 +118,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
setFilter({ cluster: pendingCluster, partition: pendingPartition });
|
setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition });
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
{#if !disableClusterSelection}
|
{#if !disableClusterSelection}
|
||||||
@@ -105,7 +128,8 @@
|
|||||||
isOpen = false;
|
isOpen = false;
|
||||||
pendingCluster = null;
|
pendingCluster = null;
|
||||||
pendingPartition = null;
|
pendingPartition = null;
|
||||||
setFilter({ cluster: pendingCluster, partition: pendingPartition})
|
pendingSubCluster = null;
|
||||||
|
setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition })
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user