diff --git a/.betterer.results b/.betterer.results index b4f15312d1b..8c565ab008f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6221,6 +6221,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -6233,6 +6236,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "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 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 or use t()", "1"], 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. + +
+
+
+ +
+
+
+ + {showHistogramExamples && ( + + )} +
+ } + + ); +} + +interface NativeHistogramExamplesButtonProps { + showHistogramExamples: boolean; + setShowHistogramExamples: Dispatch>; +} + +const NativeHistogramExamplesButton = ({ + showHistogramExamples, + setShowHistogramExamples, +}: NativeHistogramExamplesButtonProps) => { + const styles = useStyles2(getStyles, 0); + + return ( +
+ +
+ ); +}; + +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 ( + <> +
+
+
+ Now: +
+
+
+
+ Previously: +
+
+
+
+
+
+
+
+ + Native Histogram displayed as heatmap: + +
+
+ Native Histogram displayed as heatmap +
+
+
+
+ + Native Histogram displayed as histogram: + +
+
+ Native Histogram displayed as histogram +
+
+
+
+
+
+
+
+ + Classic Histogram displayed as heatmap: + +
+
+ Classic Histogram displayed as heatmap +
+
+
+
+ + Classic Histogram displayed as histogram: + +
+
+ Classic Histogram displayed as histogram +
+
+
+
+
+
+
+ + Click any of the native histograms below to explore them: + +
+
+ {nativeHistograms.map((el) => { + return ( +
+ +
+ ); + })} +
+ + ); +}; + +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" },