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>
This commit is contained in:
Brendan O'Handley
2025-01-16 03:36:10 +02:00
committed by GitHub
co-authored by Nick Richmond
parent 7da6e48036
commit 76a7987427
23 changed files with 633 additions and 20 deletions
+7
View File
@@ -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"],
+84 -9
View File
@@ -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({});
}
+2 -1
View File
@@ -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
+5 -1
View File
@@ -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_';
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+15
View File
@@ -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"
},
+15
View File
@@ -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"
},