Explore metrics: Support native histograms (#98894)
* identify native histograms by classic histograms * use trail to expose ds helper * identify native histograms for preview panel * refactor ds helper to initialize all native histograms * remove await * add info message for native histograms * hide button on show examples * show nh in metric scene by passing check for nh and storing in url for url loads, bookmarks and recent explorations * add badge for native histograms * click native histogram examples in info message to see them * add link for learn more * close banner on select when selecting native histogram in info banner * show message for newly selected data sources * capitalize Native Histogram badge * prettier * fix badge ui width * add padding for badge * add images, styling and tests for native histogram banner * move images to img folder * fix store tests * run i18n * fix betterer * fix betterer with translations * cannot translate interpolated metric in button text * Fix import * do not indent the > See examples section * trans component interferes with text with special chars * update sm text with 4px padding and 16px spacing between images * do not show banner after closing then changing data sources * prettier * Update public/app/features/trails/helpers/MetricDatasourceHelper.ts Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> * Update public/app/features/trails/banners/NativeHistogramBanner.tsx Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> * Update public/app/features/trails/banners/NativeHistogramBanner.tsx Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> * update comments * remove unnecessary code check * add rudderstack types * add close example functionality * prettier * add t() for betterer * prettier * fix betterer and trans issues * fix test --------- Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>
@@ -6221,6 +6221,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
|
||||
],
|
||||
"public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
||||
],
|
||||
"public/app/features/trails/MetricsHeader.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
@@ -6233,6 +6236,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/trails/banners/NativeHistogramBanner.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/filterByValue\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
|
||||
@@ -39,6 +39,7 @@ import { MetricScene } from './MetricScene';
|
||||
import { MetricSelectScene } from './MetricSelect/MetricSelectScene';
|
||||
import { MetricsHeader } from './MetricsHeader';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { NativeHistogramBanner } from './banners/NativeHistogramBanner';
|
||||
import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper';
|
||||
import { reportChangeInLabelFilters, reportExploreMetrics } from './interactions';
|
||||
import { migrateOtelDeploymentEnvironment } from './migrations/otelDeploymentEnvironment';
|
||||
@@ -93,10 +94,16 @@ export interface DataTrailState extends SceneObjectState {
|
||||
// Synced with url
|
||||
metric?: string;
|
||||
metricSearch?: string;
|
||||
|
||||
histogramsLoaded: boolean;
|
||||
nativeHistograms: string[];
|
||||
nativeHistogramMetric: string;
|
||||
}
|
||||
|
||||
export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneObjectWithUrlSync {
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metric', 'metricSearch', 'showPreviews'] });
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, {
|
||||
keys: ['metric', 'metricSearch', 'showPreviews', 'nativeHistogramMetric'],
|
||||
});
|
||||
|
||||
public constructor(state: Partial<DataTrailState>) {
|
||||
super({
|
||||
@@ -120,6 +127,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
// preserve the otel join query
|
||||
otelJoinQuery: state.otelJoinQuery ?? '',
|
||||
showPreviews: state.showPreviews ?? true,
|
||||
nativeHistograms: state.nativeHistograms ?? [],
|
||||
histogramsLoaded: state.histogramsLoaded ?? false,
|
||||
nativeHistogramMetric: state.nativeHistogramMetric ?? '',
|
||||
...state,
|
||||
});
|
||||
|
||||
@@ -209,6 +219,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
if (name === VAR_DATASOURCE) {
|
||||
this.datasourceHelper.reset();
|
||||
|
||||
// reset native histograms
|
||||
this.resetNativeHistograms();
|
||||
|
||||
if (this.state.afterFirstOtelCheck) {
|
||||
// we need a new check for OTel
|
||||
this.setState({ initialOtelCheckComplete: false });
|
||||
@@ -262,6 +275,33 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
return this.datasourceHelper.getMetricMetadata(metric);
|
||||
}
|
||||
|
||||
public isNativeHistogram(metric: string) {
|
||||
return this.datasourceHelper.isNativeHistogram(metric);
|
||||
}
|
||||
|
||||
// use this to initialize histograms in all scenes
|
||||
public async initializeHistograms() {
|
||||
if (!this.state.histogramsLoaded) {
|
||||
await this.datasourceHelper.initializeHistograms();
|
||||
|
||||
this.setState({
|
||||
nativeHistograms: this.listNativeHistograms(),
|
||||
histogramsLoaded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public listNativeHistograms() {
|
||||
return this.datasourceHelper.listNativeHistograms() ?? [];
|
||||
}
|
||||
|
||||
private resetNativeHistograms() {
|
||||
this.setState({
|
||||
histogramsLoaded: false,
|
||||
nativeHistograms: [],
|
||||
});
|
||||
}
|
||||
|
||||
public getCurrentMetricMetadata() {
|
||||
return this.getMetricMetadata(this.state.metric);
|
||||
}
|
||||
@@ -277,6 +317,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
history: this.state.history,
|
||||
metric: !state.metric ? undefined : state.metric,
|
||||
metricSearch: !state.metricSearch ? undefined : state.metricSearch,
|
||||
// store type because this requires an expensive api call to determine
|
||||
// when loading the metric scene
|
||||
nativeHistogramMetric: !state.nativeHistogramMetric ? undefined : state.nativeHistogramMetric,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -292,7 +335,13 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
await updateOtelJoinWithGroupLeft(this, metric);
|
||||
}
|
||||
|
||||
this.setState(this.getSceneUpdatesForNewMetricValue(metric));
|
||||
// from the metric preview panel we have the info loaded to determine that a metric is a native histogram
|
||||
let nativeHistogramMetric = false;
|
||||
if (this.isNativeHistogram(metric)) {
|
||||
nativeHistogramMetric = true;
|
||||
}
|
||||
|
||||
this.setState(this.getSceneUpdatesForNewMetricValue(metric, nativeHistogramMetric));
|
||||
|
||||
// Add metric to adhoc filters baseFilter
|
||||
const filterVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||
@@ -303,19 +352,25 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
}
|
||||
}
|
||||
|
||||
private getSceneUpdatesForNewMetricValue(metric: string | undefined) {
|
||||
private getSceneUpdatesForNewMetricValue(metric: string | undefined, nativeHistogramMetric?: boolean) {
|
||||
const stateUpdate: Partial<DataTrailState> = {};
|
||||
stateUpdate.metric = metric;
|
||||
stateUpdate.topScene = getTopSceneFor(metric);
|
||||
// refactoring opportunity? Or do we pass metric knowledge all the way down?
|
||||
// must pass this native histogram prometheus knowledge deep into
|
||||
// the topscene set on the trail > MetricScene > getAutoQueriesForMetric() > createHistogramMetricQueryDefs();
|
||||
stateUpdate.nativeHistogramMetric = nativeHistogramMetric ? '1' : '';
|
||||
stateUpdate.topScene = getTopSceneFor(metric, nativeHistogramMetric);
|
||||
return stateUpdate;
|
||||
}
|
||||
|
||||
getUrlState(): SceneObjectUrlValues {
|
||||
const { metric, metricSearch, showPreviews } = this.state;
|
||||
const { metric, metricSearch, showPreviews, nativeHistogramMetric } = this.state;
|
||||
return {
|
||||
metric,
|
||||
metricSearch,
|
||||
...{ showPreviews: showPreviews === false ? 'false' : null },
|
||||
// store the native histogram knowledge in url for the metric scene
|
||||
nativeHistogramMetric,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,7 +379,14 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
|
||||
if (typeof values.metric === 'string') {
|
||||
if (this.state.metric !== values.metric) {
|
||||
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric));
|
||||
// if we have a metric and we have stored in the url that it is a native histogram
|
||||
// we can pass that info into the metric scene to generate the appropriate queries
|
||||
let nativeHistogramMetric = false;
|
||||
if (values.nativeHistogramMetric === '1') {
|
||||
nativeHistogramMetric = true;
|
||||
}
|
||||
|
||||
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric, nativeHistogramMetric));
|
||||
}
|
||||
} else if (values.metric == null) {
|
||||
stateUpdate.metric = undefined;
|
||||
@@ -489,11 +551,23 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||
const { controls, topScene, history, settings, useOtelExperience, hasOtelResources, embedded } = model.useState();
|
||||
const {
|
||||
controls,
|
||||
topScene,
|
||||
history,
|
||||
settings,
|
||||
useOtelExperience,
|
||||
hasOtelResources,
|
||||
embedded,
|
||||
histogramsLoaded,
|
||||
nativeHistograms,
|
||||
} = model.useState();
|
||||
|
||||
const chromeHeaderHeight = useChromeHeaderHeight();
|
||||
const styles = useStyles2(getStyles, embedded ? 0 : (chromeHeaderHeight ?? 0));
|
||||
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
|
||||
// need to initialize this here and not on activate because it requires the data source helper to be fully initialized first
|
||||
model.initializeHistograms();
|
||||
|
||||
useEffect(() => {
|
||||
if (model.state.addingLabelFromBreakdown) {
|
||||
@@ -526,6 +600,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{NativeHistogramBanner({ histogramsLoaded, nativeHistograms, trail: model })}
|
||||
{showHeaderForFirstTimeUsers && <MetricsHeader />}
|
||||
<history.Component model={history} />
|
||||
{controls && (
|
||||
@@ -546,9 +621,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
|
||||
};
|
||||
}
|
||||
|
||||
export function getTopSceneFor(metric?: string) {
|
||||
export function getTopSceneFor(metric?: string, nativeHistogram?: boolean) {
|
||||
if (metric) {
|
||||
return new MetricScene({ metric: metric });
|
||||
return new MetricScene({ metric: metric, nativeHistogram: nativeHistogram ?? false });
|
||||
} else {
|
||||
return new MetricSelectScene({});
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const relatedLogsFeatureEnabled = config.featureToggles.exploreMetricsRelatedLog
|
||||
export interface MetricSceneState extends SceneObjectState {
|
||||
body: MetricGraphScene;
|
||||
metric: string;
|
||||
nativeHistogram?: boolean;
|
||||
actionView?: string;
|
||||
|
||||
autoQuery: AutoQueryInfo;
|
||||
@@ -54,7 +55,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] });
|
||||
|
||||
public constructor(state: MakeOptional<MetricSceneState, 'body' | 'autoQuery'>) {
|
||||
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
|
||||
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric, state.nativeHistogram);
|
||||
super({
|
||||
$variables: state.$variables ?? getVariableSet(state.metric),
|
||||
body: state.body ?? new MetricGraphScene({}),
|
||||
|
||||
@@ -424,7 +424,9 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> i
|
||||
children.push(metric.itemRef.resolve());
|
||||
continue;
|
||||
}
|
||||
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description);
|
||||
// refactor this into the query generator in future
|
||||
const isNative = trail.isNativeHistogram(metric.name);
|
||||
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description, isNative);
|
||||
|
||||
metric.itemRef = panel.getRef();
|
||||
metric.isPanel = true;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObjectBase } from '@grafana/scenes';
|
||||
import { Badge, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from '@grafana/ui/src/utils/i18n';
|
||||
|
||||
export class NativeHistogramBadge extends SceneObjectBase {
|
||||
public static Component = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<Badge
|
||||
className={styles.badge}
|
||||
color="blue"
|
||||
text={<Trans i18nKey="trails.metric-select.native-histogram">Native Histogram</Trans>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
badge: css({
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
border: `1px solid ${theme.colors.info.text}`,
|
||||
background: theme.colors.info.transparent,
|
||||
cursor: 'auto',
|
||||
width: '112px',
|
||||
padding: '0rem 0.25rem 0 0.35rem',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -6,20 +6,32 @@ import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../
|
||||
import { getColorByIndex } from '../utils';
|
||||
|
||||
import { AddToExplorationButton } from './AddToExplorationsButton';
|
||||
import { NativeHistogramBadge } from './NativeHistogramBadge';
|
||||
import { SelectMetricAction } from './SelectMetricAction';
|
||||
import { hideEmptyPreviews } from './hideEmptyPreviews';
|
||||
|
||||
export function getPreviewPanelFor(metric: string, index: number, currentFilterCount: number, description?: string) {
|
||||
const autoQuery = getAutoQueriesForMetric(metric);
|
||||
export function getPreviewPanelFor(
|
||||
metric: string,
|
||||
index: number,
|
||||
currentFilterCount: number,
|
||||
description?: string,
|
||||
nativeHistogram?: boolean
|
||||
) {
|
||||
const autoQuery = getAutoQueriesForMetric(metric, nativeHistogram);
|
||||
let actions: Array<SelectMetricAction | AddToExplorationButton | NativeHistogramBadge> = [
|
||||
new SelectMetricAction({ metric, title: 'Select' }),
|
||||
new AddToExplorationButton({ labelName: metric }),
|
||||
];
|
||||
|
||||
if (nativeHistogram) {
|
||||
actions.unshift(new NativeHistogramBadge({}));
|
||||
}
|
||||
|
||||
const vizPanel = autoQuery.preview
|
||||
.vizBuilder()
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(index) })
|
||||
.setDescription(description)
|
||||
.setHeaderActions([
|
||||
new SelectMetricAction({ metric, title: 'Select' }),
|
||||
new AddToExplorationButton({ labelName: metric }),
|
||||
])
|
||||
.setHeaderActions(actions)
|
||||
.build();
|
||||
|
||||
const queries = autoQuery.preview.queries.map((query) =>
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -80,6 +81,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -228,6 +230,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -242,6 +245,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -256,6 +260,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'ds',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
description: 'Test',
|
||||
@@ -506,6 +511,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -603,6 +609,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
description: 'Test',
|
||||
@@ -616,6 +623,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'time',
|
||||
description: 'Test',
|
||||
@@ -655,6 +663,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'start',
|
||||
},
|
||||
@@ -666,6 +675,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'time',
|
||||
},
|
||||
@@ -677,6 +687,7 @@ describe('TrailStore', () => {
|
||||
'var-ds': 'prom-mock',
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
nativeHistogramMetric: '',
|
||||
},
|
||||
type: 'metric',
|
||||
},
|
||||
@@ -713,6 +724,7 @@ describe('TrailStore', () => {
|
||||
history: [
|
||||
{
|
||||
urlValues: {
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -728,6 +740,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -743,6 +756,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'some_other_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
@@ -766,6 +780,7 @@ describe('TrailStore', () => {
|
||||
{
|
||||
urlValues: {
|
||||
metric: 'bookmarked_metric',
|
||||
nativeHistogramMetric: '',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
timezone,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createSummaryMetricQueryDefs } from './queryGenerators/summary';
|
||||
import { AutoQueryContext, AutoQueryInfo } from './types';
|
||||
import { getUnit } from './units';
|
||||
|
||||
export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
|
||||
export function getAutoQueriesForMetric(metric: string, nativeHistogram?: boolean): AutoQueryInfo {
|
||||
const isUtf8Metric = false;
|
||||
const metricParts = metric.split('_');
|
||||
const suffix = metricParts.at(-1);
|
||||
@@ -28,7 +28,7 @@ export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
|
||||
return createSummaryMetricQueryDefs(ctx);
|
||||
}
|
||||
|
||||
if (suffix === 'bucket') {
|
||||
if (suffix === 'bucket' || nativeHistogram) {
|
||||
return createHistogramMetricQueryDefs(ctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
import { MetricSelectedEvent } from '../shared';
|
||||
|
||||
import { NativeHistogramBanner } from './NativeHistogramBanner';
|
||||
|
||||
const mockTrail = {
|
||||
publishEvent: jest.fn(),
|
||||
} as unknown as DataTrail;
|
||||
|
||||
const mockProps = {
|
||||
histogramsLoaded: true,
|
||||
nativeHistograms: ['histogram1', 'histogram2'],
|
||||
trail: mockTrail,
|
||||
};
|
||||
|
||||
describe('NativeHistogramBanner', () => {
|
||||
test('renders correctly when histograms are loaded', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
expect(screen.getByText('Native Histogram Support')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Has a learn more button works', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
const learnMoreButton = screen.getByText('Learn more');
|
||||
expect(learnMoreButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('See examples button works', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
const seeExamplesButton = screen.getByText('> See examples');
|
||||
expect(seeExamplesButton).toBeInTheDocument();
|
||||
fireEvent.click(seeExamplesButton);
|
||||
expect(screen.getByText('Native Histogram displayed as heatmap:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Native Histogram displayed as histogram:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Classic Histogram displayed as heatmap:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Classic Histogram displayed as histogram:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Native histograms buttons work', () => {
|
||||
render(<NativeHistogramBanner {...mockProps} />);
|
||||
fireEvent.click(screen.getByText('> See examples'));
|
||||
const histogramButton = screen.getByText('histogram1');
|
||||
expect(histogramButton).toBeInTheDocument();
|
||||
fireEvent.click(histogramButton);
|
||||
expect(mockTrail.publishEvent).toHaveBeenCalledWith(new MetricSelectedEvent('histogram1'), true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useState, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, useTheme2, Alert, Button } from '@grafana/ui';
|
||||
import { t, Trans } from '@grafana/ui/src/utils/i18n';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
import { MetricSelectedEvent } from '../shared';
|
||||
|
||||
interface NativeHistogramInfoProps {
|
||||
histogramsLoaded: boolean;
|
||||
nativeHistograms: string[];
|
||||
trail: DataTrail;
|
||||
}
|
||||
|
||||
export function NativeHistogramBanner(props: NativeHistogramInfoProps) {
|
||||
const { histogramsLoaded, nativeHistograms, trail } = props;
|
||||
const [histogramMessage, setHistogramMessage] = useState(true);
|
||||
const [showHistogramExamples, setShowHistogramExamples] = useState(false);
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
|
||||
if (!histogramsLoaded || nativeHistograms.length === 0 || !histogramMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<Alert
|
||||
title={'Native Histogram Support'}
|
||||
severity={'info'}
|
||||
onRemove={() => {
|
||||
setHistogramMessage(false);
|
||||
}}
|
||||
>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={styles.histogramSentence}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.sentence">
|
||||
Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and
|
||||
a way to combine and manipulate histograms in queries and in Grafana.
|
||||
</Trans>
|
||||
</div>
|
||||
<div className={styles.histogramLearnMore}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open('https://grafana.com/docs/grafana-cloud/whats-new/native-histograms/', '_blank')
|
||||
}
|
||||
className={styles.button}
|
||||
>
|
||||
<Trans i18nKey="trails.native-histogram-banner.learn-more">Learn more</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NativeHistogramExamplesButton
|
||||
showHistogramExamples={showHistogramExamples}
|
||||
setShowHistogramExamples={setShowHistogramExamples}
|
||||
/>
|
||||
{showHistogramExamples && (
|
||||
<NativeHistogramExamples
|
||||
trail={trail}
|
||||
nativeHistograms={nativeHistograms}
|
||||
setHistogramMessage={setHistogramMessage}
|
||||
/>
|
||||
)}
|
||||
</Alert>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface NativeHistogramExamplesButtonProps {
|
||||
showHistogramExamples: boolean;
|
||||
setShowHistogramExamples: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const NativeHistogramExamplesButton = ({
|
||||
showHistogramExamples,
|
||||
setShowHistogramExamples,
|
||||
}: NativeHistogramExamplesButtonProps) => {
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className={`${styles.seeExamplesButton} native-histogram-examples-button`}
|
||||
type="button"
|
||||
fill="text"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (showHistogramExamples) {
|
||||
// hide the examples
|
||||
reportExploreMetrics('native_histogram_examples_closed', {});
|
||||
}
|
||||
setShowHistogramExamples(!showHistogramExamples);
|
||||
}}
|
||||
>
|
||||
{showHistogramExamples
|
||||
? t(`trails.native-histogram-banner.hide-examples`, `Hide examples`)
|
||||
: t(`trails.native-histogram-banner.see-examples`, `> See examples`)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type NativeHistogramExamplesProps = Pick<NativeHistogramInfoProps, 'trail' | 'nativeHistograms'> & {
|
||||
setHistogramMessage: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const NativeHistogramExamples = ({ trail, nativeHistograms, setHistogramMessage }: NativeHistogramExamplesProps) => {
|
||||
const styles = useStyles2(getStyles, 0);
|
||||
const isDark = useTheme2().isDark;
|
||||
const selectNativeHistogram = (metric: string) => {
|
||||
reportExploreMetrics('native_histogram_example_clicked', {
|
||||
metric,
|
||||
});
|
||||
trail.publishEvent(new MetricSelectedEvent(metric), true);
|
||||
};
|
||||
const images = {
|
||||
nativeHeatmap: isDark
|
||||
? 'public/img/native-histograms/DarkModeHeatmapNativeHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHeatmapNativeHistogram.png',
|
||||
classicHeatmap: isDark
|
||||
? 'public/img/native-histograms/DarkModeHeatmapClassicHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHeatmapClassicHistogram.png',
|
||||
nativeHistogram: isDark
|
||||
? 'public/img/native-histograms/DarkModeHistogramNativehistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
|
||||
classicHistogram: isDark
|
||||
? 'public/img/native-histograms/DarkModeHistogramClassicHistogram.png'
|
||||
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
|
||||
<div className={styles.histogramImageCol}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.now">Now:</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.rightCol}`}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.previously">Previously:</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
|
||||
<div className={styles.histogramImageCol}>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.nh-heatmap">
|
||||
Native Histogram displayed as heatmap:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.nativeHeatmap} alt="Native Histogram displayed as heatmap" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.nh-histogram">
|
||||
Native Histogram displayed as histogram:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.nativeHistogram} alt="Native Histogram displayed as histogram" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.rightImageCol} ${styles.rightCol}`}>
|
||||
<div className={styles.histogramRow}>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.ch-heatmap">
|
||||
Classic Histogram displayed as heatmap:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.classicHeatmap} alt="Classic Histogram displayed as heatmap" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
|
||||
<div className={styles.imageText}>
|
||||
<Trans i18nKey="trails.native-histogram-banner.ch-histogram">
|
||||
Classic Histogram displayed as histogram:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
<img width="100%" src={images.classicHistogram} alt="Classic Histogram displayed as histogram" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<Trans i18nKey="trails.native-histogram-banner.click-histogram">
|
||||
Click any of the native histograms below to explore them:
|
||||
</Trans>
|
||||
</div>
|
||||
<div>
|
||||
{nativeHistograms.map((el) => {
|
||||
return (
|
||||
<div key={el}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
selectNativeHistogram(el);
|
||||
setHistogramMessage(false);
|
||||
}}
|
||||
key={el}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fill="text"
|
||||
className={`native-histogram-example-clicked`}
|
||||
>
|
||||
{t('trails.native-histogram-banner.metric-examples', el)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2, _chromeHeaderHeight: number) {
|
||||
return {
|
||||
histogramRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
histogramSentence: css({
|
||||
width: '90%',
|
||||
}),
|
||||
histogramLearnMore: css({
|
||||
width: '10%',
|
||||
}),
|
||||
button: css({
|
||||
float: 'right',
|
||||
}),
|
||||
seeExamplesButton: css({
|
||||
paddingLeft: '0px',
|
||||
}),
|
||||
seeExamplesRow: css({
|
||||
paddingTop: '4px',
|
||||
}),
|
||||
histogramImageCol: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexBasis: '100%',
|
||||
flex: '1',
|
||||
}),
|
||||
fontSmall: css({
|
||||
fontSize: theme.typography.size.sm,
|
||||
}),
|
||||
imageText: css({
|
||||
paddingBottom: '4px',
|
||||
}),
|
||||
rightImageCol: css({
|
||||
borderLeft: `1px solid ${theme.colors.secondary.borderTransparent}`,
|
||||
}),
|
||||
rightCol: css({
|
||||
paddingLeft: '16px',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DataTrail } from '../DataTrail';
|
||||
|
||||
import { MetricDatasourceHelper } from './MetricDatasourceHelper';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
publicDashboardAccessToken: '123',
|
||||
},
|
||||
}));
|
||||
|
||||
const NATIVE_HISTOGRAM = 'test_metric';
|
||||
describe('MetricDatasourceHelper', () => {
|
||||
let metricDatasourceHelper: MetricDatasourceHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
const trail = new DataTrail({});
|
||||
metricDatasourceHelper = new MetricDatasourceHelper(trail);
|
||||
metricDatasourceHelper['_classicHistograms'] = {
|
||||
test_metric_bucket: 1,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('isNativeHistogram', () => {
|
||||
it('should return false if metric is not provided', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram('');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if metric is a native histogram', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram(NATIVE_HISTOGRAM);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if metric is not a native histogram', async () => {
|
||||
const result = await metricDatasourceHelper.isNativeHistogram('non_histogram_metric');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ export class MetricDatasourceHelper {
|
||||
public reset() {
|
||||
this._datasource = undefined;
|
||||
this._metricsMetadata = undefined;
|
||||
this._classicHistograms = {};
|
||||
this._nativeHistograms = [];
|
||||
}
|
||||
|
||||
private _trail: DataTrail;
|
||||
@@ -61,6 +63,67 @@ export class MetricDatasourceHelper {
|
||||
return metadata?.[metric];
|
||||
}
|
||||
|
||||
private _classicHistograms: Record<string, number> = {};
|
||||
private _nativeHistograms: string[] = [];
|
||||
|
||||
public listNativeHistograms() {
|
||||
return this._nativeHistograms;
|
||||
}
|
||||
/**
|
||||
* Identify native histograms by querying classic histograms and all metrics,
|
||||
* then comparing the results and build the collection of native histograms.
|
||||
*
|
||||
* classic histogram = test_metric_bucket
|
||||
* native histogram = test_metric
|
||||
*/
|
||||
public async initializeHistograms() {
|
||||
const ds = await this.getDatasource();
|
||||
if (Object.keys(this._classicHistograms).length === 0 && ds instanceof PrometheusDatasource) {
|
||||
const classicHistogramsCall = ds.metricFindQuery('metrics(.*_bucket)');
|
||||
const allMetricsCall = ds.metricFindQuery('metrics(.*)');
|
||||
|
||||
const [classicHistograms, allMetrics] = await Promise.all([classicHistogramsCall, allMetricsCall]);
|
||||
|
||||
classicHistograms.forEach((m) => {
|
||||
this._classicHistograms[m.text] = 1;
|
||||
});
|
||||
|
||||
allMetrics.forEach((m) => {
|
||||
if (this.isNativeHistogram(m.text)) {
|
||||
// Build the collection of native histograms.
|
||||
this.addNativeHistogram(m.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* If a metric name + _bucket exists in the classic histograms, then it is a native histogram
|
||||
*
|
||||
* classic histogram = test_metric_bucket
|
||||
* native histogram = test_metric
|
||||
* @param metric
|
||||
* @returns
|
||||
*/
|
||||
public isNativeHistogram(metric: string): boolean {
|
||||
if (!metric) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._classicHistograms[`${metric}_bucket`]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private addNativeHistogram(metric: string) {
|
||||
if (!this._nativeHistograms.includes(metric)) {
|
||||
this._nativeHistograms.push(metric);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for additional filtering for adhoc vars labels in Explore metrics.
|
||||
* @param options
|
||||
|
||||
@@ -131,7 +131,11 @@ type Interactions = {
|
||||
otel_experience_used: {},
|
||||
otel_experience_toggled: {
|
||||
value: ('on'| 'off')
|
||||
}
|
||||
},
|
||||
native_histogram_examples_closed: {},
|
||||
native_histogram_example_clicked: {
|
||||
metric: string;
|
||||
},
|
||||
};
|
||||
|
||||
const PREFIX = 'grafana_explore_metrics_';
|
||||
|
||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -3345,9 +3345,24 @@
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Filter by",
|
||||
"native-histogram": "Native Histogram",
|
||||
"new-badge": "New",
|
||||
"otel-switch": "This switch enables filtering by OTel resources for OTel native data sources."
|
||||
},
|
||||
"native-histogram-banner": {
|
||||
"ch-heatmap": "Classic Histogram displayed as heatmap:",
|
||||
"ch-histogram": "Classic Histogram displayed as histogram:",
|
||||
"click-histogram": "Click any of the native histograms below to explore them:",
|
||||
"hide-examples": "Hide examples",
|
||||
"learn-more": "Learn more",
|
||||
"metric-examples": "",
|
||||
"nh-heatmap": "Native Histogram displayed as heatmap:",
|
||||
"nh-histogram": "Native Histogram displayed as histogram:",
|
||||
"now": "Now:",
|
||||
"previously": "Previously:",
|
||||
"see-examples": "> See examples",
|
||||
"sentence": "Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana."
|
||||
},
|
||||
"recent-metrics": {
|
||||
"or-view-a-recent-exploration": "Or view a recent exploration"
|
||||
},
|
||||
|
||||
@@ -3345,9 +3345,24 @@
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Fįľŧęř þy",
|
||||
"native-histogram": "Ńäŧįvę Ħįşŧőģřäm",
|
||||
"new-badge": "Ńęŵ",
|
||||
"otel-switch": "Ŧĥįş şŵįŧčĥ ęʼnäþľęş ƒįľŧęřįʼnģ þy ØŦęľ řęşőūřčęş ƒőř ØŦęľ ʼnäŧįvę đäŧä şőūřčęş."
|
||||
},
|
||||
"native-histogram-banner": {
|
||||
"ch-heatmap": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
|
||||
"ch-histogram": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
|
||||
"click-histogram": "Cľįčĸ äʼny őƒ ŧĥę ʼnäŧįvę ĥįşŧőģřämş þęľőŵ ŧő ęχpľőřę ŧĥęm:",
|
||||
"hide-examples": "Ħįđę ęχämpľęş",
|
||||
"learn-more": "Ŀęäřʼn mőřę",
|
||||
"metric-examples": "",
|
||||
"nh-heatmap": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
|
||||
"nh-histogram": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
|
||||
"now": "Ńőŵ:",
|
||||
"previously": "Přęvįőūşľy:",
|
||||
"see-examples": "> Ŝęę ęχämpľęş",
|
||||
"sentence": "Přőmęŧĥęūş ʼnäŧįvę ĥįşŧőģřämş őƒƒęř ĥįģĥ řęşőľūŧįőʼn, ĥįģĥ přęčįşįőʼn, şįmpľę ūşäģę įʼn įʼnşŧřūmęʼnŧäŧįőʼn äʼnđ ä ŵäy ŧő čőmþįʼnę äʼnđ mäʼnįpūľäŧę ĥįşŧőģřämş įʼn qūęřįęş äʼnđ įʼn Ğřäƒäʼnä."
|
||||
},
|
||||
"recent-metrics": {
|
||||
"or-view-a-recent-exploration": "Øř vįęŵ ä řęčęʼnŧ ęχpľőřäŧįőʼn"
|
||||
},
|
||||
|
||||