- {hasData && (
+ {visualisationType === 'logs' && hasData && (
+ <>
+
= (props: Props) => {
renderPreview
/>
- )}
-
+
+
+
No logs found.
-
)}
{scanning && (
-
>
@@ -1067,10 +1067,17 @@ export const Logs = withTheme2(UnthemedLogs);
const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: number) => {
return {
+ noDataWrapper: css({
+ display: 'flex',
+ justifyContent: 'center',
+ width: '100%',
+ paddingBottom: theme.spacing(2),
+ }),
noData: css({
- '& > *': {
- marginLeft: '0.5em',
- },
+ display: 'inline-block',
+ }),
+ scanButton: css({
+ marginLeft: theme.spacing(1),
}),
logOptions: css({
display: 'flex',
diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts
index eb84d558a84..a3e55f10d80 100644
--- a/public/app/features/explore/state/query.test.ts
+++ b/public/app/features/explore/state/query.test.ts
@@ -323,7 +323,7 @@ describe('running queries', () => {
cleanSupplementaryQueryAction({ exploreId, type: SupplementaryQueryType.LogsSample }),
]);
});
- it('should cancel running query when a new query is issued', async () => {
+ it('should cancel running queries when a new query is issued', async () => {
const initialState = {
...makeExplorePaneState(),
};
@@ -333,6 +333,23 @@ describe('running queries', () => {
expect(dispatchedActions).toContainEqual(cancelQueriesAction({ exploreId }));
});
+ it('should not cancel running queries when scanning', async () => {
+ const initialState = {
+ ...makeExplorePaneState(),
+ explore: {
+ panes: {
+ [exploreId]: {
+ scanning: true,
+ },
+ },
+ },
+ };
+ const dispatchedActions = await thunkTester(initialState)
+ .givenThunk(runQueries)
+ .whenThunkIsDispatched({ exploreId });
+
+ expect(dispatchedActions).not.toContainEqual(cancelQueriesAction({ exploreId }));
+ });
});
describe('changeQueries', () => {
diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts
index 88181538dcd..e8ee7a86b44 100644
--- a/public/app/features/explore/state/query.ts
+++ b/public/app/features/explore/state/query.ts
@@ -528,7 +528,10 @@ interface RunQueriesOptions {
export const runQueries = createAsyncThunk
(
'explore/runQueries',
async ({ exploreId, preserveCache }, { dispatch, getState }) => {
- dispatch(cancelQueries(exploreId));
+ // Running cancel queries when Explore is scanning will cancel the scanning state
+ if (!getState().explore?.panes[exploreId]?.scanning) {
+ dispatch(cancelQueries(exploreId));
+ }
const { defaultCorrelationEditorDatasource, scopedVars, showCorrelationEditorLinks } = await getCorrelationsData(
getState(),
@@ -658,11 +661,12 @@ export const runQueries = createAsyncThunk(
// Keep scanning for results if this was the last scanning transaction
if (exploreState!.scanning) {
+ console.log(data.series);
if (data.state === LoadingState.Done && data.series.length === 0) {
const range = getShiftedTimeRange(-1, exploreState!.range);
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries({ exploreId }));
- } else {
+ } else if (data.series[0]?.length > 0 || data.state === LoadingState.Done) {
// We can stop scanning if we have a result
dispatch(scanStopAction({ exploreId }));
}
diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx
index f9f6d0bee19..46a854ffdb1 100644
--- a/public/app/features/trails/DataTrail.tsx
+++ b/public/app/features/trails/DataTrail.tsx
@@ -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 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) {
super({
@@ -120,6 +127,9 @@ export class DataTrail extends SceneObjectBase 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 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 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 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 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 implements SceneO
}
}
- private getSceneUpdatesForNewMetricValue(metric: string | undefined) {
+ private getSceneUpdatesForNewMetricValue(metric: string | undefined, nativeHistogramMetric?: boolean) {
const stateUpdate: Partial = {};
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 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 implements SceneO
}
static Component = ({ model }: SceneComponentProps) => {
- 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 implements SceneO
return (
+ {NativeHistogramBanner({ histogramsLoaded, nativeHistograms, trail: model })}
{showHeaderForFirstTimeUsers &&
}
{controls && (
@@ -546,9 +621,9 @@ export class DataTrail extends SceneObjectBase
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({});
}
diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx
index 0e3cb6b48f7..e2920e510d9 100644
--- a/public/app/features/trails/MetricScene.tsx
+++ b/public/app/features/trails/MetricScene.tsx
@@ -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 {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] });
public constructor(state: MakeOptional) {
- 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({}),
diff --git a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
index 1eb1deacf45..ce3d0c86dd3 100644
--- a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
+++ b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
@@ -424,7 +424,9 @@ export class MetricSelectScene extends SceneObjectBase 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;
diff --git a/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx
new file mode 100644
index 00000000000..f2c551f3d70
--- /dev/null
+++ b/public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx
@@ -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 (
+ Native Histogram}
+ />
+ );
+ };
+}
+
+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',
+ }),
+ };
+}
diff --git a/public/app/features/trails/MetricSelect/previewPanel.ts b/public/app/features/trails/MetricSelect/previewPanel.ts
index dd5feb9391b..92007959ac0 100644
--- a/public/app/features/trails/MetricSelect/previewPanel.ts
+++ b/public/app/features/trails/MetricSelect/previewPanel.ts
@@ -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 = [
+ 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) =>
diff --git a/public/app/features/trails/TrailStore/TrailStore.test.ts b/public/app/features/trails/TrailStore/TrailStore.test.ts
index ba41d6c6014..b92abceca20 100644
--- a/public/app/features/trails/TrailStore/TrailStore.test.ts
+++ b/public/app/features/trails/TrailStore/TrailStore.test.ts
@@ -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,
diff --git a/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts
index aa09aeb8486..732fd6ed4b3 100644
--- a/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts
+++ b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts
@@ -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);
}
diff --git a/public/app/features/trails/banners/NativeHistogramBanner.test.tsx b/public/app/features/trails/banners/NativeHistogramBanner.test.tsx
new file mode 100644
index 00000000000..d5191037dae
--- /dev/null
+++ b/public/app/features/trails/banners/NativeHistogramBanner.test.tsx
@@ -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();
+ 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();
+ const learnMoreButton = screen.getByText('Learn more');
+ expect(learnMoreButton).toBeInTheDocument();
+ });
+
+ test('See examples button works', () => {
+ render();
+ 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();
+ 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);
+ });
+});
diff --git a/public/app/features/trails/banners/NativeHistogramBanner.tsx b/public/app/features/trails/banners/NativeHistogramBanner.tsx
new file mode 100644
index 00000000000..62bcd7551f3
--- /dev/null
+++ b/public/app/features/trails/banners/NativeHistogramBanner.tsx
@@ -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 (
+ <>
+ {
+ {
+ setHistogramMessage(false);
+ }}
+ >
+
+
+
+ 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.
+
+
+
+
+
+ window.open('https://grafana.com/docs/grafana-cloud/whats-new/native-histograms/', '_blank')
+ }
+ className={styles.button}
+ >
+ Learn more
+
+
+
+
+
+ {showHistogramExamples && (
+
+ )}
+
+ }
+ >
+ );
+}
+
+interface NativeHistogramExamplesButtonProps {
+ showHistogramExamples: boolean;
+ setShowHistogramExamples: Dispatch>;
+}
+
+const NativeHistogramExamplesButton = ({
+ showHistogramExamples,
+ setShowHistogramExamples,
+}: NativeHistogramExamplesButtonProps) => {
+ const styles = useStyles2(getStyles, 0);
+
+ return (
+
+ {
+ 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`)}
+
+
+ );
+};
+
+type NativeHistogramExamplesProps = Pick & {
+ setHistogramMessage: Dispatch>;
+};
+
+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 (
+ <>
+
+
+
+
+
+
+
+ Native Histogram displayed as heatmap:
+
+
+
+

+
+
+
+
+
+ Native Histogram displayed as histogram:
+
+
+
+

+
+
+
+
+
+
+
+
+
+ Classic Histogram displayed as heatmap:
+
+
+
+

+
+
+
+
+
+ Classic Histogram displayed as histogram:
+
+
+
+

+
+
+
+
+
+
+
+
+ Click any of the native histograms below to explore them:
+
+
+
+ {nativeHistograms.map((el) => {
+ return (
+
+ {
+ 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)}
+
+
+ );
+ })}
+
+ >
+ );
+};
+
+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',
+ }),
+ };
+}
diff --git a/public/app/features/trails/helpers/MetricDataSourceHelper.test.ts b/public/app/features/trails/helpers/MetricDataSourceHelper.test.ts
new file mode 100644
index 00000000000..48574cebe20
--- /dev/null
+++ b/public/app/features/trails/helpers/MetricDataSourceHelper.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/public/app/features/trails/helpers/MetricDatasourceHelper.ts b/public/app/features/trails/helpers/MetricDatasourceHelper.ts
index d9b84d76b82..4ecfb09db53 100644
--- a/public/app/features/trails/helpers/MetricDatasourceHelper.ts
+++ b/public/app/features/trails/helpers/MetricDatasourceHelper.ts
@@ -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 = {};
+ 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
diff --git a/public/app/features/trails/interactions.ts b/public/app/features/trails/interactions.ts
index 968742feb35..c0306b01410 100644
--- a/public/app/features/trails/interactions.ts
+++ b/public/app/features/trails/interactions.ts
@@ -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_';
diff --git a/public/img/native-histograms/DarkModeHeatmapClassicHistogram.png b/public/img/native-histograms/DarkModeHeatmapClassicHistogram.png
new file mode 100644
index 00000000000..df27290a806
Binary files /dev/null and b/public/img/native-histograms/DarkModeHeatmapClassicHistogram.png differ
diff --git a/public/img/native-histograms/DarkModeHeatmapNativeHistogram.png b/public/img/native-histograms/DarkModeHeatmapNativeHistogram.png
new file mode 100644
index 00000000000..cc2b7c4e73c
Binary files /dev/null and b/public/img/native-histograms/DarkModeHeatmapNativeHistogram.png differ
diff --git a/public/img/native-histograms/DarkModeHistogramClassicHistogram.png b/public/img/native-histograms/DarkModeHistogramClassicHistogram.png
new file mode 100644
index 00000000000..0d9bcc27f8c
Binary files /dev/null and b/public/img/native-histograms/DarkModeHistogramClassicHistogram.png differ
diff --git a/public/img/native-histograms/DarkModeHistogramNativehistogram.png b/public/img/native-histograms/DarkModeHistogramNativehistogram.png
new file mode 100644
index 00000000000..ec4512312fc
Binary files /dev/null and b/public/img/native-histograms/DarkModeHistogramNativehistogram.png differ
diff --git a/public/img/native-histograms/LightModeHeatmapClassicHistogram.png b/public/img/native-histograms/LightModeHeatmapClassicHistogram.png
new file mode 100644
index 00000000000..3f4c08afcc7
Binary files /dev/null and b/public/img/native-histograms/LightModeHeatmapClassicHistogram.png differ
diff --git a/public/img/native-histograms/LightModeHeatmapNativeHistogram.png b/public/img/native-histograms/LightModeHeatmapNativeHistogram.png
new file mode 100644
index 00000000000..8737db3a8cc
Binary files /dev/null and b/public/img/native-histograms/LightModeHeatmapNativeHistogram.png differ
diff --git a/public/img/native-histograms/LightModeHistogramClassicHistogram.png b/public/img/native-histograms/LightModeHistogramClassicHistogram.png
new file mode 100644
index 00000000000..4c1bcd9e671
Binary files /dev/null and b/public/img/native-histograms/LightModeHistogramClassicHistogram.png differ
diff --git a/public/img/native-histograms/LightModeHistogramNativeHistogram.png b/public/img/native-histograms/LightModeHistogramNativeHistogram.png
new file mode 100644
index 00000000000..539841315c8
Binary files /dev/null and b/public/img/native-histograms/LightModeHistogramNativeHistogram.png differ
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 9f2a6569ae2..ae10eb7c4e2 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -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"
},
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 118e80758e0..705aa03d6e9 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -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"
},