mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore Metrics: Implement grouping with metric prefixes (#89481)
* add groop as a local dependency * update layout * nested layout with panels * fix the height of the rows * copy groop library into grafana/grafana * Don't create a new scene everytime metrics refreshed * Add display option dropdown * handle different layout options in buildLayout * add select component props * unify scene body creation * handle other display cases in refreshMetricNames * set a new body when display format is different * handle nestedScene population * show nested groups * handle panel display * add tabs view * populate tabs view * show selected tab group * show display options before metric search * populate prefix filter layout * only switch layout for nested-rows display option * Update public/app/features/trails/groop/parser.ts Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> * Update public/app/features/trails/groop/parser.ts Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> * Update public/app/features/trails/MetricSelect/MetricSelectScene.tsx Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> * Update public/app/features/trails/MetricSelect/MetricSelectScene.tsx Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> * Remove tab view * generate groups async * Remove unnecessary parts * Refactor * implement urlSync * update keys * introduce interaction * ui updates * chore: revert some auto formatting to clarify comments * chore: revert some auto formatting to clarify comments * rename * add tooltip * add styles * update unit tests * make i18n-extract * update unit test --------- Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Co-authored-by: Darren Janeczek <darren.janeczek@grafana.com>
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
import { useReducer } from 'react';
|
||||
import * as React from 'react';
|
||||
import { SyntheticEvent, useReducer } from 'react';
|
||||
|
||||
import { GrafanaTheme2, RawTimeRange } from '@grafana/data';
|
||||
import { GrafanaTheme2, RawTimeRange, SelectableValue } from '@grafana/data';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
@@ -12,21 +11,28 @@ import {
|
||||
SceneCSSGridItem,
|
||||
SceneCSSGridLayout,
|
||||
SceneFlexItem,
|
||||
SceneFlexLayout,
|
||||
sceneGraph,
|
||||
SceneObject,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
SceneObjectWithUrlSync,
|
||||
SceneTimeRange,
|
||||
SceneVariable,
|
||||
SceneVariableSet,
|
||||
VariableDependencyConfig,
|
||||
} from '@grafana/scenes';
|
||||
import { InlineSwitch, Field, Alert, Icon, useStyles2, Tooltip, Input } from '@grafana/ui';
|
||||
import { Alert, Field, Icon, IconButton, InlineSwitch, Input, Select, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
import { MetricScene } from '../MetricScene';
|
||||
import { StatusWrapper } from '../StatusWrapper';
|
||||
import { Node, Parser } from '../groop/parser';
|
||||
import { getMetricDescription } from '../helpers/MetricDatasourceHelper';
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
import {
|
||||
@@ -54,7 +60,9 @@ interface MetricPanel {
|
||||
}
|
||||
|
||||
export interface MetricSelectSceneState extends SceneObjectState {
|
||||
body: SceneCSSGridLayout;
|
||||
body: SceneFlexLayout | SceneCSSGridLayout;
|
||||
rootGroup?: Node;
|
||||
metricPrefix?: string;
|
||||
showPreviews?: boolean;
|
||||
metricNames?: string[];
|
||||
metricNamesLoading?: boolean;
|
||||
@@ -64,16 +72,23 @@ export interface MetricSelectSceneState extends SceneObjectState {
|
||||
|
||||
const ROW_PREVIEW_HEIGHT = '175px';
|
||||
const ROW_CARD_HEIGHT = '64px';
|
||||
const METRIC_PREFIX_ALL = 'all';
|
||||
|
||||
const MAX_METRIC_NAMES = 20000;
|
||||
|
||||
export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
const viewByTooltip =
|
||||
'View by the metric prefix. A metric prefix is a single word at the beginning of the metric name, relevant to the domain the metric belongs to.';
|
||||
|
||||
export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> implements SceneObjectWithUrlSync {
|
||||
private previewCache: Record<string, MetricPanel> = {};
|
||||
private ignoreNextUpdate = false;
|
||||
private _debounceRefreshMetricNames = debounce(() => this._refreshMetricNames(), 1000);
|
||||
|
||||
constructor(state: Partial<MetricSelectSceneState>) {
|
||||
super({
|
||||
showPreviews: true,
|
||||
$variables: state.$variables,
|
||||
metricPrefix: state.metricPrefix ?? METRIC_PREFIX_ALL,
|
||||
body:
|
||||
state.body ??
|
||||
new SceneCSSGridLayout({
|
||||
@@ -82,13 +97,13 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
autoRows: ROW_PREVIEW_HEIGHT,
|
||||
isLazy: true,
|
||||
}),
|
||||
showPreviews: true,
|
||||
...state,
|
||||
});
|
||||
|
||||
this.addActivationHandler(this._onActivate.bind(this));
|
||||
}
|
||||
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metricPrefix'] });
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
variableNames: [VAR_DATASOURCE, VAR_FILTERS],
|
||||
onReferencedVariableValueChanged: (variable: SceneVariable) => {
|
||||
@@ -97,6 +112,18 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
},
|
||||
});
|
||||
|
||||
getUrlState() {
|
||||
return { metricPrefix: this.state.metricPrefix };
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues) {
|
||||
if (typeof values.metricPrefix === 'string') {
|
||||
if (this.state.metricPrefix !== values.metricPrefix) {
|
||||
this.setState({ metricPrefix: values.metricPrefix });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
if (this.state.body.state.children.length === 0) {
|
||||
this.buildLayout();
|
||||
@@ -159,8 +186,6 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
this._debounceRefreshMetricNames();
|
||||
}
|
||||
|
||||
private _debounceRefreshMetricNames = debounce(() => this._refreshMetricNames(), 1000);
|
||||
|
||||
private async _refreshMetricNames() {
|
||||
const trail = getTrailFor(this);
|
||||
const timeRange: RawTimeRange | undefined = trail.state.$timeRange?.state;
|
||||
@@ -199,7 +224,17 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
`Add search terms or label filters to narrow down the number of metric names returned.`
|
||||
: undefined;
|
||||
|
||||
this.setState({ metricNames, metricNamesLoading: false, metricNamesWarning, metricNamesError: response.error });
|
||||
let bodyLayout = this.state.body;
|
||||
const rootGroupNode = await this.generateGroups(metricNames);
|
||||
|
||||
this.setState({
|
||||
metricNames,
|
||||
rootGroup: rootGroupNode,
|
||||
body: bodyLayout,
|
||||
metricNamesLoading: false,
|
||||
metricNamesWarning,
|
||||
metricNamesError: response.error,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
let error = 'Unknown error';
|
||||
if (isFetchError(err)) {
|
||||
@@ -214,19 +249,16 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
}
|
||||
}
|
||||
|
||||
private sortedPreviewMetrics() {
|
||||
return Object.values(this.previewCache).sort((a, b) => {
|
||||
if (a.isEmpty && b.isEmpty) {
|
||||
return a.index - b.index;
|
||||
}
|
||||
if (a.isEmpty) {
|
||||
return 1;
|
||||
}
|
||||
if (b.isEmpty) {
|
||||
return -1;
|
||||
}
|
||||
return a.index - b.index;
|
||||
});
|
||||
private async generateGroups(metricNames: string[] = []) {
|
||||
const groopParser = new Parser();
|
||||
groopParser.config = {
|
||||
...groopParser.config,
|
||||
maxDepth: 2,
|
||||
minGroupSize: 2,
|
||||
miscGroupKey: 'misc',
|
||||
};
|
||||
const { root: rootGroupNode } = groopParser.parse(metricNames);
|
||||
return rootGroupNode;
|
||||
}
|
||||
|
||||
private onMetricNamesChanged() {
|
||||
@@ -286,32 +318,67 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const children: SceneFlexItem[] = [];
|
||||
if (!this.state.rootGroup) {
|
||||
const rootGroupNode = await this.generateGroups(this.state.metricNames);
|
||||
this.setState({ rootGroup: rootGroupNode });
|
||||
}
|
||||
|
||||
const children = await this.populateFilterableViewLayout();
|
||||
const rowTemplate = this.state.showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT;
|
||||
this.state.body.setState({ children, autoRows: rowTemplate });
|
||||
}
|
||||
|
||||
private async populateFilterableViewLayout() {
|
||||
const trail = getTrailFor(this);
|
||||
|
||||
const metricsList = this.sortedPreviewMetrics();
|
||||
|
||||
// Get the current filters to determine the count of them
|
||||
// Which is required for `getPreviewPanelFor`
|
||||
const filters = getFilters(this);
|
||||
|
||||
let rootGroupNode = this.state.rootGroup;
|
||||
if (!rootGroupNode) {
|
||||
rootGroupNode = await this.generateGroups(this.state.metricNames);
|
||||
this.setState({ rootGroup: rootGroupNode });
|
||||
}
|
||||
|
||||
const children: SceneFlexItem[] = [];
|
||||
|
||||
for (const [groupKey, groupNode] of rootGroupNode.groups) {
|
||||
if (this.state.metricPrefix !== METRIC_PREFIX_ALL && this.state.metricPrefix !== groupKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [_, value] of groupNode.groups) {
|
||||
const panels = await this.populatePanels(trail, filters, value.values);
|
||||
children.push(...panels);
|
||||
}
|
||||
|
||||
const morePanelsMaybe = await this.populatePanels(trail, filters, groupNode.values);
|
||||
children.push(...morePanelsMaybe);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
private async populatePanels(trail: DataTrail, filters: ReturnType<typeof getFilters>, values: string[]) {
|
||||
const currentFilterCount = filters?.length || 0;
|
||||
|
||||
for (let index = 0; index < metricsList.length; index++) {
|
||||
const metric = metricsList[index];
|
||||
const metadata = await trail.getMetricMetadata(metric.name);
|
||||
const previewPanelLayoutItems: SceneFlexItem[] = [];
|
||||
for (let index = 0; index < values.length; index++) {
|
||||
const metricName = values[index];
|
||||
const metric: MetricPanel = this.previewCache[metricName] ?? { name: metricName, index, loaded: false };
|
||||
const metadata = await trail.getMetricMetadata(metricName);
|
||||
const description = getMetricDescription(metadata);
|
||||
|
||||
if (this.state.showPreviews) {
|
||||
if (metric.itemRef && metric.isPanel) {
|
||||
children.push(metric.itemRef.resolve());
|
||||
previewPanelLayoutItems.push(metric.itemRef.resolve());
|
||||
continue;
|
||||
}
|
||||
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description);
|
||||
|
||||
metric.itemRef = panel.getRef();
|
||||
metric.isPanel = true;
|
||||
children.push(panel);
|
||||
previewPanelLayoutItems.push(panel);
|
||||
} else {
|
||||
const panel = new SceneCSSGridItem({
|
||||
$variables: new SceneVariableSet({
|
||||
@@ -321,13 +388,11 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
});
|
||||
metric.itemRef = panel.getRef();
|
||||
metric.isPanel = false;
|
||||
children.push(panel);
|
||||
previewPanelLayoutItems.push(panel);
|
||||
}
|
||||
}
|
||||
|
||||
const rowTemplate = this.state.showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT;
|
||||
|
||||
this.state.body.setState({ children, autoRows: rowTemplate });
|
||||
return previewPanelLayoutItems;
|
||||
}
|
||||
|
||||
public updateMetricPanel = (metric: string, isLoaded?: boolean, isEmpty?: boolean) => {
|
||||
@@ -336,25 +401,52 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
metricPanel.isEmpty = isEmpty;
|
||||
metricPanel.loaded = isLoaded;
|
||||
this.previewCache[metric] = metricPanel;
|
||||
this.buildLayout();
|
||||
if (this.state.metricPrefix === 'All') {
|
||||
this.buildLayout();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public onSearchQueryChange = (evt: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
public onSearchQueryChange = (evt: SyntheticEvent<HTMLInputElement>) => {
|
||||
const metricSearch = evt.currentTarget.value;
|
||||
const trail = getTrailFor(this);
|
||||
// Update the variable
|
||||
trail.setState({ metricSearch });
|
||||
};
|
||||
|
||||
public onPrefixFilterChange = (val: SelectableValue) => {
|
||||
this.setState({ metricPrefix: val.value });
|
||||
this.buildLayout();
|
||||
};
|
||||
|
||||
public reportPrefixFilterInteraction = (isMenuOpen: boolean) => {
|
||||
const trail = getTrailFor(this);
|
||||
const { steps, currentStep } = trail.state.history.state;
|
||||
const previousMetric = steps[currentStep]?.trailState.metric;
|
||||
const isRelatedMetricSelector = previousMetric !== undefined;
|
||||
|
||||
reportExploreMetrics('prefix_filter_clicked', {
|
||||
from: isRelatedMetricSelector ? 'related_metrics' : 'metric_list',
|
||||
action: isMenuOpen ? 'open' : 'close',
|
||||
});
|
||||
};
|
||||
|
||||
public onTogglePreviews = () => {
|
||||
this.setState({ showPreviews: !this.state.showPreviews });
|
||||
this.buildLayout();
|
||||
};
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<MetricSelectScene>) => {
|
||||
const { showPreviews, body, metricNames, metricNamesError, metricNamesLoading, metricNamesWarning } =
|
||||
model.useState();
|
||||
const {
|
||||
showPreviews,
|
||||
body,
|
||||
metricNames,
|
||||
metricNamesError,
|
||||
metricNamesLoading,
|
||||
metricNamesWarning,
|
||||
rootGroup,
|
||||
metricPrefix,
|
||||
} = model.useState();
|
||||
const { children } = body.useState();
|
||||
const trail = getTrailFor(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -399,6 +491,29 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
suffix={metricNamesWarningIcon}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={
|
||||
<div className={styles.displayOptionTooltip}>
|
||||
<Trans i18nKey="explore-metrics.viewBy">View by</Trans>
|
||||
<IconButton name={'info-circle'} size="sm" variant={'secondary'} tooltip={viewByTooltip} />
|
||||
</div>
|
||||
}
|
||||
className={styles.displayOption}
|
||||
>
|
||||
<Select
|
||||
value={metricPrefix}
|
||||
onChange={model.onPrefixFilterChange}
|
||||
onOpenMenu={() => model.reportPrefixFilterInteraction(true)}
|
||||
onCloseMenu={() => model.reportPrefixFilterInteraction(false)}
|
||||
options={[
|
||||
{
|
||||
label: 'All metric names',
|
||||
value: METRIC_PREFIX_ALL,
|
||||
},
|
||||
...Array.from(rootGroup?.groups.keys() ?? []).map((g) => ({ label: `${g}_`, value: g })),
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<InlineSwitch showLabel={true} label="Show previews" value={showPreviews} onChange={model.onTogglePreviews} />
|
||||
</div>
|
||||
{metricNamesError && (
|
||||
@@ -418,7 +533,8 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
</Alert>
|
||||
)}
|
||||
<StatusWrapper {...{ isLoading, blockingMessage }}>
|
||||
<body.Component model={body} />
|
||||
{body instanceof SceneFlexLayout && <body.Component model={body} />}
|
||||
{body instanceof SceneCSSGridLayout && <body.Component model={body} />}
|
||||
</StatusWrapper>
|
||||
</div>
|
||||
);
|
||||
@@ -439,7 +555,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}),
|
||||
headingWrapper: css({
|
||||
marginBottom: theme.spacing(0.5),
|
||||
@@ -455,6 +570,18 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
flexGrow: 1,
|
||||
marginBottom: 0,
|
||||
}),
|
||||
metricTabGroup: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
displayOption: css({
|
||||
flexGrow: 0,
|
||||
marginBottom: 0,
|
||||
minWidth: '184px',
|
||||
}),
|
||||
displayOptionTooltip: css({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
warningIcon: css({
|
||||
color: theme.colors.warning.main,
|
||||
}),
|
||||
|
||||
88
public/app/features/trails/groop/lookup.test.ts
Normal file
88
public/app/features/trails/groop/lookup.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { collectValues, lookup, lookupNode } from './lookup';
|
||||
import { Parser } from './parser';
|
||||
|
||||
const metrics = readFileSync(join(__dirname, 'testdata', 'metrics.txt'), 'utf8').split('\n');
|
||||
|
||||
describe('lookup', () => {
|
||||
it('should find exact group matches', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const group1 = lookup(grouping.root, 'agent');
|
||||
expect(group1).not.toBeNull();
|
||||
expect(group1!.descendants).toBe(41);
|
||||
expect(group1!.groups.size).toBe(1);
|
||||
expect(group1!.values.length).toBe(0);
|
||||
const allValues = collectValues(group1!);
|
||||
expect(allValues.length).toBe(41);
|
||||
});
|
||||
|
||||
it('should find groups and values that contain all of the prefix', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const group1 = lookup(grouping.root, 'age');
|
||||
expect(group1).not.toBeNull();
|
||||
expect(group1!.descendants).toBe(41);
|
||||
expect(group1!.groups.size).toBe(1);
|
||||
expect(group1!.values.length).toBe(0);
|
||||
const allValues = collectValues(group1!);
|
||||
expect(allValues.length).toBe(41);
|
||||
});
|
||||
|
||||
it('should find nested groups and values that match', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const matches = lookup(grouping.root, 'agent_metrics_c');
|
||||
expect(matches).not.toBeNull();
|
||||
expect(matches!.descendants).toBe(9);
|
||||
const allValues = collectValues(matches!);
|
||||
expect(allValues.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupNode', () => {
|
||||
it('should find matching groups by prefix', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const group1 = lookupNode(grouping.root, 'agent');
|
||||
expect(group1).not.toBeNull();
|
||||
expect(group1!.descendants).toBe(41);
|
||||
expect(group1!.groups.size).toBe(3);
|
||||
expect(group1!.values.length).toBe(8);
|
||||
|
||||
const group2 = lookupNode(grouping.root, 'agent_metrics_ha_configs');
|
||||
expect(group2).not.toBeNull();
|
||||
expect(group2!.descendants).toBe(6);
|
||||
expect(group2!.values.length).toBe(6);
|
||||
expect(group2!.values[0]).toBe('agent_metrics_ha_configs_created');
|
||||
expect(group2!.values[1]).toBe('agent_metrics_ha_configs_created_total');
|
||||
expect(group2!.values[2]).toBe('agent_metrics_ha_configs_deleted');
|
||||
expect(group2!.values[3]).toBe('agent_metrics_ha_configs_deleted_total');
|
||||
expect(group2!.values[4]).toBe('agent_metrics_ha_configs_updated');
|
||||
expect(group2!.values[5]).toBe('agent_metrics_ha_configs_updated_total');
|
||||
});
|
||||
|
||||
it('should be able to collect all values', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const group1 = lookupNode(grouping.root, 'agent');
|
||||
expect(group1).not.toBeNull();
|
||||
const allValues = collectValues(group1!);
|
||||
expect(allValues.length).toBe(41);
|
||||
|
||||
// check a sample of values
|
||||
expect(allValues).toContain('agent_config_hash');
|
||||
expect(allValues).toContain('agent_metrics_active_instances');
|
||||
expect(allValues).toContain('agent_metrics_ha_configs_created_total');
|
||||
expect(allValues).toContain('agent_tcp_connections_limit');
|
||||
expect(allValues).toContain('agent_wal_storage_created_series_total');
|
||||
});
|
||||
});
|
||||
75
public/app/features/trails/groop/lookup.ts
Normal file
75
public/app/features/trails/groop/lookup.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export { lookupNode, lookup, collectValues };
|
||||
|
||||
import { prefixDelimited } from './parser';
|
||||
|
||||
interface Node {
|
||||
groups: Map<string, Node>;
|
||||
values: string[];
|
||||
descendants: number;
|
||||
}
|
||||
|
||||
// lookup finds the node matching the prefix.
|
||||
// If no match is found, it returns null.
|
||||
function lookupNode(node: Node, prefix: string): Node | null {
|
||||
return _lookupNode(node, 0, prefix);
|
||||
}
|
||||
|
||||
// _lookupNode is the recursive implementation of lookup.
|
||||
function _lookupNode(node: Node, level: number, prefix: string): Node | null {
|
||||
const thisPrefix = prefixDelimited(prefix, level);
|
||||
for (const [k, group] of node.groups.entries()) {
|
||||
if (k === prefix) {
|
||||
// perfect match
|
||||
return group;
|
||||
}
|
||||
if (k.startsWith(thisPrefix)) {
|
||||
return _lookupNode(group, level + 1, prefix);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function lookup(node: Node, prefix: string): Node | null {
|
||||
const groups = new Map<string, Node>();
|
||||
const values: string[] = [];
|
||||
let descendants = 0;
|
||||
for (const [nodePrefix, group] of node.groups.entries()) {
|
||||
if (nodePrefix.startsWith(prefix)) {
|
||||
groups.set(nodePrefix, group);
|
||||
descendants += group.descendants;
|
||||
continue;
|
||||
}
|
||||
if (prefix.startsWith(nodePrefix)) {
|
||||
const subGroup = lookup(group, prefix);
|
||||
if (subGroup) {
|
||||
groups.set(nodePrefix, subGroup);
|
||||
descendants += subGroup.descendants;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const v of node.values) {
|
||||
if (v.startsWith(prefix)) {
|
||||
values.push(v);
|
||||
descendants += 1;
|
||||
}
|
||||
}
|
||||
if (groups.size === 0 && values.length === 0) {
|
||||
// nothing partially matching - so just return null
|
||||
return null;
|
||||
}
|
||||
return { groups, values, descendants };
|
||||
}
|
||||
|
||||
// collectValues returns all values from the node and its descendants.
|
||||
function collectValues(node: Node): string[] {
|
||||
const values: string[] = []; // Specify the type of the 'values' array
|
||||
function collectFrom(currentNode: Node): void {
|
||||
values.push(...currentNode.values);
|
||||
for (const groupNode of currentNode.groups.values()) {
|
||||
collectFrom(groupNode);
|
||||
}
|
||||
}
|
||||
|
||||
collectFrom(node);
|
||||
return values;
|
||||
}
|
||||
90
public/app/features/trails/groop/parser.test.ts
Normal file
90
public/app/features/trails/groop/parser.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { lookupNode } from './lookup';
|
||||
import { Parser, prefixDelimited } from './parser';
|
||||
|
||||
describe('Parser', () => {
|
||||
const metrics = readFileSync(join(__dirname, 'testdata', 'metrics.txt'), 'utf8').split('\n');
|
||||
|
||||
it('should parse strings and build a tree', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
expect(grouping.root.groups.has('agent')).toBe(true);
|
||||
const agentGroup = grouping.root.groups.get('agent')!;
|
||||
|
||||
expect(agentGroup.descendants).toBe(41);
|
||||
|
||||
expect(agentGroup.groups.size).toBe(3);
|
||||
expect(agentGroup.groups.has('agent_config')).toBe(true);
|
||||
expect(agentGroup.groups.has('agent_metrics')).toBe(true);
|
||||
expect(agentGroup.groups.has('agent_wal')).toBe(true);
|
||||
|
||||
// get all keys from agentGroup.groups
|
||||
const groupKeys = Array.from(agentGroup.groups.keys());
|
||||
expect(groupKeys.length).toBe(3);
|
||||
expect(groupKeys[0]).toBe('agent_config');
|
||||
expect(groupKeys[1]).toBe('agent_metrics');
|
||||
expect(groupKeys[2]).toBe('agent_wal');
|
||||
|
||||
// check the values
|
||||
expect(agentGroup.values.length).toBe(8);
|
||||
expect(agentGroup.values[0]).toBe('agent');
|
||||
expect(agentGroup.values[1]).toBe('agent_build_info');
|
||||
expect(agentGroup.values[2]).toBe('agent_inflight_requests');
|
||||
expect(agentGroup.values[3]).toBe('agent_request_duration_seconds');
|
||||
expect(agentGroup.values[4]).toBe('agent_request_message_bytes');
|
||||
expect(agentGroup.values[5]).toBe('agent_response_message_bytes');
|
||||
expect(agentGroup.values[6]).toBe('agent_tcp_connections');
|
||||
expect(agentGroup.values[7]).toBe('agent_tcp_connections_limit');
|
||||
});
|
||||
|
||||
it('should put metrics that match the name of the group in that group', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
const grouping = parser.parse(metrics);
|
||||
const agentGroup = lookupNode(grouping.root, 'agent');
|
||||
expect(agentGroup).not.toBeNull();
|
||||
expect(agentGroup!.values.length).toBe(8);
|
||||
expect(agentGroup!.values[0]).toBe('agent');
|
||||
});
|
||||
|
||||
it('should respect maxDepth', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
parser.config.maxDepth = 1;
|
||||
const grouping = parser.parse(metrics);
|
||||
|
||||
const agentGroup = lookupNode(grouping.root, 'agent');
|
||||
expect(agentGroup).not.toBeNull();
|
||||
expect(agentGroup!.values.length).toBe(41);
|
||||
});
|
||||
|
||||
it('should support misc for left-over values', () => {
|
||||
const parser = new Parser();
|
||||
parser.config.idealMaxGroupSize = 5;
|
||||
parser.config.maxDepth = 1;
|
||||
parser.config.miscGroupKey = 'misc';
|
||||
const grouping = parser.parse(metrics);
|
||||
expect(grouping.root.descendants).toBe(263);
|
||||
expect(grouping.root.values.length).toBe(0);
|
||||
|
||||
const miscGroup = lookupNode(grouping.root, 'misc');
|
||||
expect(miscGroup).not.toBeNull();
|
||||
expect(miscGroup!.values.length).toBe(17);
|
||||
expect(miscGroup?.descendants).toBe(17);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prefixDelimited', () => {
|
||||
it('should return the prefix of a string', () => {
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 0)).toBe('agent');
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 1)).toBe('agent_metrics');
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 2)).toBe('agent_metrics_ha');
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 3)).toBe('agent_metrics_ha_configs');
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 4)).toBe('agent_metrics_ha_configs_created');
|
||||
expect(prefixDelimited('agent_metrics_ha_configs_created', 5)).toBe('agent_metrics_ha_configs_created');
|
||||
});
|
||||
});
|
||||
138
public/app/features/trails/groop/parser.ts
Normal file
138
public/app/features/trails/groop/parser.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
export { Parser, prefixDelimited };
|
||||
|
||||
interface Config {
|
||||
minGroupSize: number;
|
||||
idealMaxGroupSize: number;
|
||||
maxDepth: number;
|
||||
miscGroupKey?: string;
|
||||
}
|
||||
|
||||
interface Grouping {
|
||||
root: Node;
|
||||
}
|
||||
|
||||
class Parser {
|
||||
public config: Config;
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
minGroupSize: 3,
|
||||
idealMaxGroupSize: 30,
|
||||
maxDepth: 100,
|
||||
};
|
||||
}
|
||||
|
||||
parse(values: string[]): Grouping {
|
||||
if (this.config.maxDepth <= 0) {
|
||||
throw new Error('Max depth must be greater than 0');
|
||||
}
|
||||
if (this.config.minGroupSize < 1) {
|
||||
throw new Error('Min group size must be greater than 0');
|
||||
}
|
||||
if (this.config.idealMaxGroupSize < this.config.minGroupSize) {
|
||||
throw new Error('Max group size must be greater than min group size');
|
||||
}
|
||||
if (this.config.miscGroupKey && this.config.miscGroupKey === '') {
|
||||
throw new Error('miscGroupKey cannot be empty');
|
||||
}
|
||||
const root = this.parseStrings(values, 0);
|
||||
return { root };
|
||||
}
|
||||
|
||||
parseStrings(values: string[], level: number): Node {
|
||||
const node: Node = {
|
||||
groups: new Map(),
|
||||
values: [],
|
||||
descendants: 0,
|
||||
};
|
||||
// go through each value and group it by its
|
||||
// prefix, for the current level
|
||||
for (const value of values) {
|
||||
// skip empty values
|
||||
if (value.trim() === '') {
|
||||
continue;
|
||||
}
|
||||
const prefix = prefixDelimited(value, level);
|
||||
// do we have this group?
|
||||
let group = node.groups.get(prefix);
|
||||
if (!group) {
|
||||
// create a new group
|
||||
group = { groups: new Map(), values: [], descendants: 0 };
|
||||
node.groups.set(prefix, group);
|
||||
}
|
||||
// add the value to the group
|
||||
group.values.push(value);
|
||||
group.descendants++;
|
||||
}
|
||||
// check if we need to collapse or split any groups
|
||||
const miscGroupValues: string[] = [];
|
||||
for (let [key, group] of node.groups.entries()) {
|
||||
if (group.values.length < this.config.minGroupSize) {
|
||||
// this group is too small
|
||||
if (this.config.miscGroupKey) {
|
||||
// put the values into the miscGroupValues
|
||||
miscGroupValues.push(...group.values);
|
||||
} else {
|
||||
// put the values into the node itself
|
||||
node.values.push(...group.values);
|
||||
}
|
||||
// keep track of the descendants
|
||||
node.descendants += group.values.length;
|
||||
// remove the group
|
||||
node.groups.delete(key);
|
||||
} else if (group.values.length > this.config.idealMaxGroupSize && level < this.config.maxDepth - 1) {
|
||||
// this group is too big - see if we can split it into
|
||||
// subgroups
|
||||
group = this.parseStrings(group.values, level + 1);
|
||||
node.groups.set(key, group);
|
||||
node.descendants += group.descendants;
|
||||
} else {
|
||||
node.descendants += group.descendants;
|
||||
}
|
||||
}
|
||||
if (this.config.miscGroupKey && miscGroupValues.length > 0) {
|
||||
// looks like we have some values for a misc group
|
||||
const group: Node = {
|
||||
groups: new Map(),
|
||||
values: miscGroupValues,
|
||||
descendants: miscGroupValues.length,
|
||||
};
|
||||
node.groups.set(this.config.miscGroupKey, group);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
groups: Map<string, Node>;
|
||||
values: string[];
|
||||
descendants: number;
|
||||
}
|
||||
|
||||
// PrefixDelimited extracts the prefix of a string at the given level.
|
||||
function prefixDelimited(s: string, level: number): string {
|
||||
let delimiterCount = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const char = s.charCodeAt(i);
|
||||
// Check if the character is not a letter or digit (non-alphanumeric)
|
||||
if (
|
||||
!(
|
||||
(
|
||||
(char >= 0x30 && char <= 0x39) || // 0-9
|
||||
(char >= 0x41 && char <= 0x5a) || // A-Z
|
||||
(char >= 0x61 && char <= 0x7a) || // a-z
|
||||
(char >= 0xc0 && char <= 0xd6) || // Latin-1 Supplement and Extended-A
|
||||
(char >= 0xd8 && char <= 0xf6) || // Latin-1 Supplement and Extended-A
|
||||
(char >= 0xf8 && char <= 0xff) || // Latin-1 Supplement and Extended-A
|
||||
(char >= 0x0100 && char <= 0x017f)
|
||||
) // Latin Extended-A
|
||||
)
|
||||
) {
|
||||
delimiterCount++;
|
||||
if (delimiterCount > level) {
|
||||
return s.slice(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return s; // Return the entire string if the level is higher than the number of delimiters.
|
||||
}
|
||||
263
public/app/features/trails/groop/testdata/metrics.txt
vendored
Normal file
263
public/app/features/trails/groop/testdata/metrics.txt
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
adhoc_channel_total
|
||||
agent
|
||||
agent_build_info
|
||||
agent_config_hash
|
||||
agent_config_last_load_success_timestamp_seconds
|
||||
agent_config_last_load_successful
|
||||
agent_config_load_failures
|
||||
agent_config_load_failures_total
|
||||
agent_inflight_requests
|
||||
agent_metrics_active_configs
|
||||
agent_metrics_active_instances
|
||||
agent_metrics_cleaner_abandoned_storage
|
||||
agent_metrics_cleaner_cleanup_seconds
|
||||
agent_metrics_cleaner_errors
|
||||
agent_metrics_cleaner_errors_total
|
||||
agent_metrics_cleaner_managed_storage
|
||||
agent_metrics_cleaner_success
|
||||
agent_metrics_cleaner_success_total
|
||||
agent_metrics_configs_changed
|
||||
agent_metrics_configs_changed_total
|
||||
agent_metrics_ha_configs_created
|
||||
agent_metrics_ha_configs_created_total
|
||||
agent_metrics_ha_configs_deleted
|
||||
agent_metrics_ha_configs_deleted_total
|
||||
agent_metrics_ha_configs_updated
|
||||
agent_metrics_ha_configs_updated_total
|
||||
agent_request_duration_seconds
|
||||
agent_request_message_bytes
|
||||
agent_response_message_bytes
|
||||
agent_tcp_connections
|
||||
agent_tcp_connections_limit
|
||||
agent_wal_exemplars_appended
|
||||
agent_wal_exemplars_appended_total
|
||||
agent_wal_out_of_order_samples
|
||||
agent_wal_samples_appended
|
||||
agent_wal_samples_appended_total
|
||||
agent_wal_storage_active_series
|
||||
agent_wal_storage_created_series
|
||||
agent_wal_storage_created_series_total
|
||||
agent_wal_storage_deleted_series
|
||||
agent_wal_storage_removed_series
|
||||
agent_wal_storage_removed_series_total
|
||||
aggregator_discovery_aggregation_count_total
|
||||
aggregator_openapi_v2_regeneration_count
|
||||
aggregator_openapi_v2_regeneration_duration
|
||||
aggregator_unavailable_apiservice
|
||||
aggregator_unavailable_apiservice_total
|
||||
apiextensions_openapi_v2_regeneration_count
|
||||
apiextensions_openapi_v3_regeneration_count
|
||||
apiserver_admission_controller_admission_duration_seconds
|
||||
apiserver_admission_step_admission_duration_seconds
|
||||
apiserver_admission_step_admission_duration_seconds_summary
|
||||
apiserver_admission_webhook_admission_duration_seconds
|
||||
apiserver_admission_webhook_fail_open_count
|
||||
apiserver_admission_webhook_rejection_count
|
||||
apiserver_admission_webhook_request_total
|
||||
apiserver_audit_event_total
|
||||
apiserver_audit_requests_rejected_total
|
||||
apiserver_cache_list_fetched_objects_total
|
||||
apiserver_cache_list_returned_objects_total
|
||||
apiserver_cache_list_total
|
||||
apiserver_cel_compilation_duration_seconds
|
||||
apiserver_cel_evaluation_duration_seconds
|
||||
apiserver_client_certificate_expiration_seconds
|
||||
apiserver_crd_webhook_conversion_duration_seconds
|
||||
apiserver_current_inflight_requests
|
||||
apiserver_current_inqueue_requests
|
||||
apiserver_delegated_authn_request_duration_seconds
|
||||
apiserver_delegated_authn_request_total
|
||||
apiserver_delegated_authz_request_duration_seconds
|
||||
apiserver_delegated_authz_request_total
|
||||
apiserver_egress_dialer_dial_duration_seconds
|
||||
apiserver_egress_dialer_dial_failure_count
|
||||
apiserver_egress_dialer_dial_start_total
|
||||
apiserver_envelope_encryption_dek_cache_fill_percent
|
||||
apiserver_flowcontrol_current_executing_requests
|
||||
apiserver_flowcontrol_current_inqueue_requests
|
||||
apiserver_flowcontrol_current_limit_seats
|
||||
apiserver_flowcontrol_current_r
|
||||
apiserver_flowcontrol_demand_seats
|
||||
apiserver_flowcontrol_demand_seats_average
|
||||
apiserver_flowcontrol_demand_seats_high_watermark
|
||||
apiserver_flowcontrol_demand_seats_smoothed
|
||||
apiserver_flowcontrol_demand_seats_stdev
|
||||
apiserver_flowcontrol_dispatch_r
|
||||
apiserver_flowcontrol_dispatched_requests_total
|
||||
apiserver_flowcontrol_latest_s
|
||||
apiserver_flowcontrol_lower_limit_seats
|
||||
apiserver_flowcontrol_next_discounted_s_bounds
|
||||
apiserver_flowcontrol_next_s_bounds
|
||||
apiserver_flowcontrol_nominal_limit_seats
|
||||
apiserver_flowcontrol_priority_level_request_utilization
|
||||
apiserver_flowcontrol_priority_level_seat_utilization
|
||||
apiserver_flowcontrol_read_vs_write_current_requests
|
||||
apiserver_flowcontrol_request_concurrency_in_use
|
||||
apiserver_flowcontrol_request_concurrency_limit
|
||||
apiserver_flowcontrol_request_execution_seconds
|
||||
apiserver_flowcontrol_request_queue_length_after_enqueue
|
||||
apiserver_flowcontrol_request_wait_duration_seconds
|
||||
apiserver_flowcontrol_seat_fair_frac
|
||||
apiserver_flowcontrol_target_seats
|
||||
apiserver_flowcontrol_upper_limit_seats
|
||||
apiserver_flowcontrol_watch_count_samples
|
||||
apiserver_flowcontrol_work_estimated_seats
|
||||
apiserver_init_events_total
|
||||
apiserver_kube_aggregator_x509_insecure_sha1_total
|
||||
apiserver_kube_aggregator_x509_missing_san_total
|
||||
apiserver_longrunning_requests
|
||||
apiserver_request_aborts_total
|
||||
apiserver_request_duration_seconds
|
||||
apiserver_request_filter_duration_seconds
|
||||
apiserver_request_post_timeout_total
|
||||
apiserver_request_sli_duration_seconds
|
||||
apiserver_request_slo_duration_seconds
|
||||
apiserver_request_terminations_total
|
||||
apiserver_request_timestamp_comparison_time
|
||||
apiserver_request_total
|
||||
apiserver_requested_deprecated_apis
|
||||
apiserver_response_sizes
|
||||
apiserver_selfrequest_total
|
||||
apiserver_storage_data_key_generation_duration_seconds
|
||||
apiserver_storage_data_key_generation_failures_total
|
||||
apiserver_storage_db_total_size_in_bytes
|
||||
apiserver_storage_decode_errors_total
|
||||
apiserver_storage_envelope_transformation_cache_misses_total
|
||||
apiserver_storage_list_evaluated_objects_total
|
||||
apiserver_storage_list_fetched_objects_total
|
||||
apiserver_storage_list_returned_objects_total
|
||||
apiserver_storage_list_total
|
||||
apiserver_storage_objects
|
||||
apiserver_terminated_watchers_total
|
||||
apiserver_tls_handshake_errors_total
|
||||
apiserver_watch_cache_events_dispatched_total
|
||||
apiserver_watch_cache_events_received_total
|
||||
apiserver_watch_cache_initializations_total
|
||||
apiserver_watch_events_sizes
|
||||
apiserver_watch_events_total
|
||||
apiserver_webhooks_x509_insecure_sha1_total
|
||||
apiserver_webhooks_x509_missing_san_total
|
||||
attachdetach_controller_forced_detaches
|
||||
authenticated_user_requests
|
||||
authentication_attempts
|
||||
authentication_duration_seconds
|
||||
authentication_token_cache_active_fetch_count
|
||||
authentication_token_cache_fetch_total
|
||||
authentication_token_cache_request_duration_seconds
|
||||
authentication_token_cache_request_total
|
||||
blackbox_exporter_config_last_reload_success_timestamp_seconds
|
||||
blackbox_exporter_config_last_reload_successful
|
||||
blackbox_module_unknown_total
|
||||
cadvisor_version_info
|
||||
certwatcher_read_certificate_errors_total
|
||||
certwatcher_read_certificate_total
|
||||
config_reload_errors
|
||||
config_reload_spurious
|
||||
config_reload_total
|
||||
conntrack_exporter_build_info
|
||||
conntrack_pod_connections
|
||||
conntrack_pod_ip_table_retrieve_duration_seconds
|
||||
conntrack_table_read_duration_seconds
|
||||
container_blkio_device_usage_total
|
||||
container_cpu_cfs_periods_total
|
||||
container_cpu_cfs_throttled_periods_total
|
||||
container_cpu_cfs_throttled_seconds_total
|
||||
container_cpu_load_average_10s
|
||||
container_cpu_system_seconds_total
|
||||
container_cpu_usage_seconds_total
|
||||
container_cpu_user_seconds_total
|
||||
container_file_descriptors
|
||||
container_fs_inodes_free
|
||||
container_fs_inodes_total
|
||||
container_fs_io_current
|
||||
container_fs_io_time_seconds_total
|
||||
container_fs_io_time_weighted_seconds_total
|
||||
container_fs_limit_bytes
|
||||
container_fs_read_seconds_total
|
||||
container_fs_reads_bytes_total
|
||||
container_fs_reads_merged_total
|
||||
container_fs_reads_total
|
||||
container_fs_sector_reads_total
|
||||
container_fs_sector_writes_total
|
||||
container_fs_usage_bytes
|
||||
container_fs_write_seconds_total
|
||||
container_fs_writes_bytes_total
|
||||
container_fs_writes_merged_total
|
||||
container_fs_writes_total
|
||||
container_last_seen
|
||||
container_memory_cache
|
||||
container_memory_failcnt
|
||||
container_memory_failures_total
|
||||
container_memory_mapped_file
|
||||
container_memory_max_usage_bytes
|
||||
container_memory_rss
|
||||
container_memory_swap
|
||||
container_memory_usage_bytes
|
||||
container_memory_working_set_bytes
|
||||
container_network_receive_bytes_total
|
||||
container_network_receive_errors_total
|
||||
container_network_receive_packets_dropped_total
|
||||
container_network_receive_packets_total
|
||||
container_network_transmit_bytes_total
|
||||
container_network_transmit_errors_total
|
||||
container_network_transmit_packets_dropped_total
|
||||
container_network_transmit_packets_total
|
||||
container_oom_events_total
|
||||
container_processes
|
||||
container_scrape_error
|
||||
container_sockets
|
||||
container_spec_cpu_period
|
||||
container_spec_cpu_quota
|
||||
container_spec_cpu_shares
|
||||
container_spec_memory_limit_bytes
|
||||
container_spec_memory_reservation_limit_bytes
|
||||
container_spec_memory_swap_limit_bytes
|
||||
container_start_time_seconds
|
||||
container_tasks_state
|
||||
container_threads
|
||||
container_threads_max
|
||||
container_ulimits_soft
|
||||
controller_runtime_active_workers
|
||||
controller_runtime_max_concurrent_reconciles
|
||||
controller_runtime_reconcile_errors_total
|
||||
controller_runtime_reconcile_time_seconds
|
||||
controller_runtime_reconcile_total
|
||||
controller_runtime_webhook_latency_seconds
|
||||
controller_runtime_webhook_requests_in_flight
|
||||
controller_runtime_webhook_requests_total
|
||||
coredns_build_info
|
||||
coredns_cache_entries
|
||||
coredns_cache_hits_total
|
||||
coredns_cache_misses_total
|
||||
coredns_cache_requests_total
|
||||
coredns_dns_request_duration_seconds
|
||||
coredns_dns_request_size_bytes
|
||||
coredns_dns_requests_total
|
||||
coredns_dns_response_size_bytes
|
||||
coredns_dns_responses_total
|
||||
coredns_forward_conn_cache_hits_total
|
||||
coredns_forward_conn_cache_misses_total
|
||||
coredns_forward_healthcheck_broken_total
|
||||
coredns_forward_healthcheck_failures_total
|
||||
coredns_forward_max_concurrent_rejects_total
|
||||
coredns_forward_request_duration_seconds
|
||||
coredns_forward_requests_total
|
||||
coredns_forward_responses_total
|
||||
coredns_health_request_duration_seconds
|
||||
coredns_health_request_failures_total
|
||||
coredns_hosts_entries
|
||||
coredns_hosts_reload_timestamp_seconds
|
||||
coredns_kubernetes_dns_programming_duration_seconds
|
||||
coredns_local_localhost_requests_total
|
||||
coredns_panics_total
|
||||
coredns_plugin_enabled
|
||||
coredns_reload_failed_total
|
||||
cortex_experimental_features_in_use
|
||||
cortex_experimental_features_in_use_total
|
||||
cronjob_controller_job_creation_skew_duration_seconds
|
||||
csi_operations_seconds
|
||||
deprecated_flags_inuse
|
||||
deprecated_flags_inuse_total
|
||||
disabled_metric_total
|
||||
ecs_cpu_seconds_total
|
||||
ecs_cpu_utilized
|
||||
@@ -95,6 +95,21 @@ type Interactions = {
|
||||
// The number of search terms activated when the selection was made
|
||||
searchTermCount: number | null;
|
||||
};
|
||||
// User opens/closes the prefix filter dropdown
|
||||
prefix_filter_clicked: {
|
||||
from: (
|
||||
// By clicking "Select" on a metric panel when on the no-metric-selected metrics list view
|
||||
| 'metric_list'
|
||||
// By clicking "Select" on a metric panel when on the related metrics tab
|
||||
| 'related_metrics'
|
||||
)
|
||||
action: (
|
||||
// Opens the dropdown
|
||||
| 'open'
|
||||
// Closes the dropdown
|
||||
| 'close'
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const PREFIX = 'grafana_explore_metrics_';
|
||||
|
||||
@@ -792,6 +792,9 @@
|
||||
"split-widen": "Widen pane"
|
||||
}
|
||||
},
|
||||
"explore-metrics": {
|
||||
"viewBy": "View by"
|
||||
},
|
||||
"export": {
|
||||
"json": {
|
||||
"cancel-button": "Cancel",
|
||||
|
||||
@@ -792,6 +792,9 @@
|
||||
"split-widen": "Ŵįđęʼn päʼnę"
|
||||
}
|
||||
},
|
||||
"explore-metrics": {
|
||||
"viewBy": "Vįęŵ þy"
|
||||
},
|
||||
"export": {
|
||||
"json": {
|
||||
"cancel-button": "Cäʼnčęľ",
|
||||
|
||||
Reference in New Issue
Block a user