Tempo: Remove aggregate by (#98474)
* Remove aggregate by * Update betterer * Run toggles test * Add back group by in schema so it can be checked for * Show error if group by * Update error message * Allow user to remove the group by from the query * Fix assertion --------- Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
@@ -6557,9 +6557,6 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/tempo/SearchTraceQLEditor/DurationInput.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"]
|
||||
],
|
||||
|
||||
@@ -150,7 +150,6 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
|
||||
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers |
|
||||
| `mlExpressions` | Enable support for Machine Learning in server-side expressions |
|
||||
| `metricsSummary` | Enables metrics summary queries in the Tempo data source |
|
||||
| `datasourceAPIServers` | Expose some datasources as apiservers. |
|
||||
| `provisioning` | Next generation provisioning... and git |
|
||||
| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder |
|
||||
|
||||
@@ -271,10 +271,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
traceQLStreaming?: boolean;
|
||||
/**
|
||||
* Enables metrics summary queries in the Tempo data source
|
||||
*/
|
||||
metricsSummary?: boolean;
|
||||
/**
|
||||
* Expose some datasources as apiservers.
|
||||
*/
|
||||
datasourceAPIServers?: boolean;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export interface TempoQuery extends common.DataQuery {
|
||||
exemplars?: number;
|
||||
filters: Array<TraceqlFilter>;
|
||||
/**
|
||||
* Filters that are used to query the metrics summary
|
||||
* deprecated Filters that are used to query the metrics summary
|
||||
*/
|
||||
groupBy?: Array<TraceqlFilter>;
|
||||
/**
|
||||
|
||||
@@ -441,13 +441,6 @@ var (
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "metricsSummary",
|
||||
Description: "Enables metrics summary queries in the Tempo data source",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
},
|
||||
{
|
||||
Name: "datasourceAPIServers",
|
||||
Description: "Expose some datasources as apiservers.",
|
||||
|
||||
@@ -58,7 +58,6 @@ awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,
|
||||
transformationsRedesign,GA,@grafana/observability-metrics,false,false,true
|
||||
mlExpressions,experimental,@grafana/alerting-squad,false,false,false
|
||||
traceQLStreaming,GA,@grafana/observability-traces-and-profiling,false,false,true
|
||||
metricsSummary,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||
datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
|
||||
|
@@ -243,10 +243,6 @@ const (
|
||||
// Enables response streaming of TraceQL queries of the Tempo data source
|
||||
FlagTraceQLStreaming = "traceQLStreaming"
|
||||
|
||||
// FlagMetricsSummary
|
||||
// Enables metrics summary queries in the Tempo data source
|
||||
FlagMetricsSummary = "metricsSummary"
|
||||
|
||||
// FlagDatasourceAPIServers
|
||||
// Expose some datasources as apiservers.
|
||||
FlagDatasourceAPIServers = "datasourceAPIServers"
|
||||
|
||||
@@ -2768,7 +2768,8 @@
|
||||
"metadata": {
|
||||
"name": "metricsSummary",
|
||||
"resourceVersion": "1718727528075",
|
||||
"creationTimestamp": "2023-08-28T14:02:12Z"
|
||||
"creationTimestamp": "2023-08-28T14:02:12Z",
|
||||
"deletionTimestamp": "2024-11-25T10:47:18Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables metrics summary queries in the Tempo data source",
|
||||
|
||||
@@ -48,7 +48,7 @@ type TempoQuery struct {
|
||||
// Defines the maximum number of spans per spanset that are returned from Tempo
|
||||
Spss *int64 `json:"spss,omitempty"`
|
||||
Filters []TraceqlFilter `json:"filters"`
|
||||
// Filters that are used to query the metrics summary
|
||||
// deprecated Filters that are used to query the metrics summary
|
||||
GroupBy []TraceqlFilter `json:"groupBy,omitempty"`
|
||||
// The type of the table that is used to display the search results
|
||||
TableType *SearchTableType `json:"tableType,omitempty"`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Button } from '@grafana/ui';
|
||||
|
||||
import { TempoQuery } from '../dataquery.gen';
|
||||
|
||||
export function AggregateByAlert({
|
||||
query,
|
||||
onChange,
|
||||
}: {
|
||||
query: TempoQuery;
|
||||
onChange?: () => void;
|
||||
}): React.ReactNode | null {
|
||||
return query.groupBy ? (
|
||||
<Alert title="" severity="info">
|
||||
The aggregate by feature has been removed. We recommend using Traces Drildown app instead.
|
||||
<Button onClick={onChange}>Remove aggregate by from this query</Button>
|
||||
</Alert>
|
||||
) : null;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import TempoLanguageProvider from '../language_provider';
|
||||
import { initTemplateSrv } from '../test/test_utils';
|
||||
import { TempoQuery } from '../types';
|
||||
|
||||
import { GroupByField } from './GroupByField';
|
||||
|
||||
describe('GroupByField', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
const datasource: TempoDatasource = {
|
||||
search: {
|
||||
filters: [],
|
||||
},
|
||||
} as unknown as TempoDatasource;
|
||||
|
||||
const lp = new TempoLanguageProvider(datasource);
|
||||
datasource.languageProvider = lp;
|
||||
|
||||
let query: TempoQuery = {
|
||||
refId: 'A',
|
||||
queryType: 'traceqlSearch',
|
||||
query: '',
|
||||
filters: [],
|
||||
groupBy: [{ id: 'group-by-id', scope: TraceqlSearchScope.Span, tag: 'component' }],
|
||||
};
|
||||
|
||||
const onChange = (q: TempoQuery) => {
|
||||
query = q;
|
||||
};
|
||||
|
||||
jest.spyOn(lp, 'getMetricsSummaryTags').mockReturnValue(['component', 'http.method', 'http.status_code']);
|
||||
jest.spyOn(lp, 'getTags').mockReturnValue(['component', 'http.method', 'http.status_code']);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// Need to use delay: null here to work with fakeTimers
|
||||
// see https://github.com/testing-library/user-event/issues/833
|
||||
user = userEvent.setup({ delay: null });
|
||||
|
||||
initTemplateSrv([{ name: 'templateVariable1' }, { name: 'templateVariable2' }], {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should only show add/remove tag when necessary', async () => {
|
||||
const GroupByWithProps = () => {
|
||||
const [query, setQuery] = useState<TempoQuery>({
|
||||
refId: 'A',
|
||||
queryType: 'traceqlSearch',
|
||||
key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0',
|
||||
filters: [],
|
||||
groupBy: [{ id: 'group-by-id', scope: TraceqlSearchScope.Span }],
|
||||
});
|
||||
return (
|
||||
<GroupByField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={(q: TempoQuery) => setQuery(q)}
|
||||
isTagsLoading={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
render(<GroupByWithProps />);
|
||||
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||
expect(screen.getAllByText('Select tag').length).toBe(1);
|
||||
|
||||
await user.click(screen.getByText('Select tag'));
|
||||
jest.advanceTimersByTime(1000);
|
||||
await user.click(screen.getByText('http.method'));
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(screen.getAllByLabelText('Add tag').length).toBe(1);
|
||||
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(1);
|
||||
|
||||
await user.click(screen.getByLabelText('Add tag'));
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the new tag, so no need to add another one
|
||||
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(2); // one for each tag
|
||||
|
||||
await user.click(screen.getAllByLabelText(/Remove tag/)[1]);
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(screen.queryAllByLabelText('Add tag').length).toBe(1); // filled in the default tag, so can add another one
|
||||
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(1); // filled in the default tag, so can remove values
|
||||
|
||||
await user.click(screen.getAllByLabelText(/Remove tag/)[0]);
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||
});
|
||||
|
||||
it('should update scope when new value is selected in scope input', async () => {
|
||||
const { container } = render(
|
||||
<GroupByField datasource={datasource} query={query} onChange={onChange} isTagsLoading={false} />
|
||||
);
|
||||
|
||||
const scopeSelect = container.querySelector(`input[aria-label="Select scope for filter 1"]`);
|
||||
expect(scopeSelect).not.toBeNull();
|
||||
expect(scopeSelect).toBeInTheDocument();
|
||||
|
||||
if (scopeSelect) {
|
||||
await user.click(scopeSelect);
|
||||
jest.advanceTimersByTime(1000);
|
||||
const resourceScope = await screen.findByText('resource');
|
||||
await user.click(resourceScope);
|
||||
const groupByFilter = query.groupBy?.find((f) => f.id === 'group-by-id');
|
||||
expect(groupByFilter).not.toBeNull();
|
||||
expect(groupByFilter?.scope).toBe('resource');
|
||||
expect(groupByFilter?.tag).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should update tag when new value is selected in tag input', async () => {
|
||||
const { container } = render(
|
||||
<GroupByField datasource={datasource} query={query} onChange={onChange} isTagsLoading={false} />
|
||||
);
|
||||
|
||||
const tagSelect = container.querySelector(`input[aria-label="Select tag for filter 1"]`);
|
||||
expect(tagSelect).not.toBeNull();
|
||||
expect(tagSelect).toBeInTheDocument();
|
||||
|
||||
if (tagSelect) {
|
||||
await user.click(tagSelect);
|
||||
jest.advanceTimersByTime(1000);
|
||||
const tag = await screen.findByText('http.method');
|
||||
await user.click(tag);
|
||||
const groupByFilter = query.groupBy?.find((f) => f.id === 'group-by-id');
|
||||
expect(groupByFilter).not.toBeNull();
|
||||
expect(groupByFilter?.tag).toBe('http.method');
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow selecting template variables', async () => {
|
||||
const { container } = render(
|
||||
<GroupByField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
isTagsLoading={false}
|
||||
addVariablesToOptions={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const tagSelect = container.querySelector(`input[aria-label="Select tag for filter 1"]`);
|
||||
expect(tagSelect).not.toBeNull();
|
||||
expect(tagSelect).toBeInTheDocument();
|
||||
|
||||
if (tagSelect) {
|
||||
await user.click(tagSelect);
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(await screen.findByText('$templateVariable1')).toBeInTheDocument();
|
||||
expect(await screen.findByText('$templateVariable2')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { AccessoryButton } from '@grafana/plugin-ui';
|
||||
import { Alert, HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import { OPTIONS_LIMIT } from '../language_provider';
|
||||
import { TempoQuery } from '../types';
|
||||
|
||||
import InlineSearchField from './InlineSearchField';
|
||||
import { withTemplateVariableOptions } from './SearchField';
|
||||
import { replaceAt } from './utils';
|
||||
|
||||
interface Props {
|
||||
datasource: TempoDatasource;
|
||||
onChange: (value: TempoQuery) => void;
|
||||
query: Partial<TempoQuery> & TempoQuery;
|
||||
isTagsLoading: boolean;
|
||||
addVariablesToOptions?: boolean;
|
||||
}
|
||||
|
||||
export const GroupByField = (props: Props) => {
|
||||
const { datasource, onChange, query, isTagsLoading, addVariablesToOptions } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const generateId = () => uuidv4().slice(0, 8);
|
||||
const [tagQuery, setTagQuery] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.groupBy || query.groupBy.length === 0) {
|
||||
onChange({
|
||||
...query,
|
||||
groupBy: [
|
||||
{
|
||||
id: generateId(),
|
||||
scope: TraceqlSearchScope.Span,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [onChange, query]);
|
||||
|
||||
const tagOptions = useMemo(
|
||||
() => (f: TraceqlFilter) => {
|
||||
const tags = datasource!.languageProvider.getMetricsSummaryTags(f.scope);
|
||||
if (tagQuery.length === 0) {
|
||||
return tags.slice(0, OPTIONS_LIMIT);
|
||||
}
|
||||
|
||||
const queryLowerCase = tagQuery.toLowerCase();
|
||||
return tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, OPTIONS_LIMIT);
|
||||
},
|
||||
[datasource, tagQuery]
|
||||
);
|
||||
|
||||
const addFilter = () => {
|
||||
updateFilter({
|
||||
id: generateId(),
|
||||
scope: TraceqlSearchScope.Span,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFilter = (filter: TraceqlFilter) => {
|
||||
onChange({ ...query, groupBy: query.groupBy?.filter((f) => f.id !== filter.id) });
|
||||
};
|
||||
|
||||
const updateFilter = (filter: TraceqlFilter) => {
|
||||
const copy = { ...query };
|
||||
copy.groupBy ||= [];
|
||||
const indexOfFilter = copy.groupBy.findIndex((f) => f.id === filter.id);
|
||||
if (indexOfFilter >= 0) {
|
||||
copy.groupBy = replaceAt(copy.groupBy, indexOfFilter, filter);
|
||||
} else {
|
||||
copy.groupBy.push(filter);
|
||||
}
|
||||
onChange(copy);
|
||||
};
|
||||
|
||||
const scopeOptions = Object.values(TraceqlSearchScope)
|
||||
.filter((s) => {
|
||||
// only add scope if it has tags
|
||||
return datasource.languageProvider.getTags(s).length > 0;
|
||||
})
|
||||
.map((t) => ({ label: t, value: t }));
|
||||
|
||||
return (
|
||||
<InlineSearchField
|
||||
label="Aggregate by"
|
||||
tooltip={`Note: We recommend using Explore Traces instead. Select one or more tags to see the metrics summary.`}
|
||||
>
|
||||
<>
|
||||
{query.groupBy?.map((f, i) => {
|
||||
const tags = tagOptions(f)
|
||||
?.concat(f.tag !== undefined && f.tag !== '' && !tagOptions(f)?.includes(f.tag) ? [f.tag] : [])
|
||||
.map((t) => ({
|
||||
label: t,
|
||||
value: t,
|
||||
}));
|
||||
return (
|
||||
<div key={f.id}>
|
||||
<HorizontalGroup spacing={'none'} width={'auto'}>
|
||||
<Select
|
||||
aria-label={`Select scope for filter ${i + 1}`}
|
||||
onChange={(v) => {
|
||||
updateFilter({ ...f, scope: v?.value, tag: '' });
|
||||
}}
|
||||
options={scopeOptions}
|
||||
placeholder="Select scope"
|
||||
value={f.scope}
|
||||
/>
|
||||
<Select
|
||||
aria-label={`Select tag for filter ${i + 1}`}
|
||||
isClearable
|
||||
allowCustomValue
|
||||
isLoading={isTagsLoading}
|
||||
key={f.tag}
|
||||
onChange={(v) => {
|
||||
updateFilter({ ...f, tag: v?.value });
|
||||
}}
|
||||
options={addVariablesToOptions ? withTemplateVariableOptions(tags) : tags}
|
||||
onInputChange={(value: string, { action }: InputActionMeta) => {
|
||||
if (action === 'input-change') {
|
||||
setTagQuery(value);
|
||||
}
|
||||
}}
|
||||
onCloseMenu={() => setTagQuery('')}
|
||||
placeholder="Select tag"
|
||||
value={f.tag || ''}
|
||||
/>
|
||||
{(f.tag || (query.groupBy?.length ?? 0) > 1) && (
|
||||
<AccessoryButton
|
||||
aria-label={`Remove tag for filter ${i + 1}`}
|
||||
icon="times"
|
||||
onClick={() => removeFilter(f)}
|
||||
tooltip="Remove tag"
|
||||
title={`Remove tag for filter ${i + 1}`}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
{f.tag && i === (query.groupBy?.length ?? 0) - 1 && (
|
||||
<span className={styles.addTag}>
|
||||
<AccessoryButton
|
||||
aria-label="Add tag"
|
||||
icon="plus"
|
||||
onClick={() => addFilter()}
|
||||
tooltip="Add tag"
|
||||
variant="secondary"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{query.groupBy && query.groupBy.length > 0 && query.groupBy[0].tag && (
|
||||
<Alert title="" severity="warning" className={styles.notice}>
|
||||
The aggregate by feature is deprecated. We recommend using Explore Traces instead. If you want to write your
|
||||
own TraceQL queries to replicate this API, please check
|
||||
<a
|
||||
href={
|
||||
'https://grafana.com/docs/tempo/latest/api_docs/metrics-summary/#deprecation-in-favor-of-traceql-metrics'
|
||||
}
|
||||
className={styles.noticeLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
this page
|
||||
</a>
|
||||
.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
</InlineSearchField>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
addTag: css({
|
||||
marginLeft: theme.spacing(1),
|
||||
}),
|
||||
notice: css({
|
||||
width: '500px',
|
||||
marginTop: theme.spacing(0.75),
|
||||
}),
|
||||
noticeLink: css({
|
||||
color: theme.colors.text.link,
|
||||
textDecoration: 'underline',
|
||||
marginLeft: '5px',
|
||||
}),
|
||||
});
|
||||
@@ -2,8 +2,6 @@ import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import TempoLanguageProvider from '../language_provider';
|
||||
@@ -238,26 +236,28 @@ describe('TraceQLSearch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render group by when feature toggle is not enabled', async () => {
|
||||
await waitFor(() => {
|
||||
it('should not render group by alert when query does not contain group by', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TraceQLSearch datasource={datasource} query={query} onChange={onChange} onClearResults={onClearResults} />
|
||||
);
|
||||
const groupBy = screen.queryByText('Aggregate by');
|
||||
expect(groupBy).toBeNull();
|
||||
expect(groupBy).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Remove aggregate by from this query' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render group by when feature toggle enabled', async () => {
|
||||
config.featureToggles.metricsSummary = true;
|
||||
await waitFor(() => {
|
||||
it('should render group by alert when query contains group by', async () => {
|
||||
const onChange = jest.fn();
|
||||
await waitFor(async () => {
|
||||
render(
|
||||
<TraceQLSearch datasource={datasource} query={query} onChange={onChange} onClearResults={onClearResults} />
|
||||
<TraceQLSearch
|
||||
datasource={datasource}
|
||||
query={{ ...query, groupBy: [] }}
|
||||
onChange={onChange}
|
||||
onClearResults={onClearResults}
|
||||
/>
|
||||
);
|
||||
const groupBy = screen.queryByText('Aggregate by');
|
||||
expect(groupBy).not.toBeNull();
|
||||
expect(groupBy).toBeInTheDocument();
|
||||
const button = screen.queryByRole('button', { name: 'Remove aggregate by from this query' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,8 +13,8 @@ import { TempoQueryBuilderOptions } from '../traceql/TempoQueryBuilderOptions';
|
||||
import { traceqlGrammar } from '../traceql/traceql';
|
||||
import { TempoQuery } from '../types';
|
||||
|
||||
import { AggregateByAlert } from './AggregateByAlert';
|
||||
import DurationInput from './DurationInput';
|
||||
import { GroupByField } from './GroupByField';
|
||||
import InlineSearchField from './InlineSearchField';
|
||||
import SearchField from './SearchField';
|
||||
import TagsInput from './TagsInput';
|
||||
@@ -237,15 +237,15 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
|
||||
addVariablesToOptions={addVariablesToOptions}
|
||||
/>
|
||||
</InlineSearchField>
|
||||
{config.featureToggles.metricsSummary && (
|
||||
<GroupByField
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
query={query}
|
||||
isTagsLoading={isTagsLoading}
|
||||
addVariablesToOptions={addVariablesToOptions}
|
||||
/>
|
||||
)}
|
||||
<AggregateByAlert
|
||||
query={query}
|
||||
onChange={() => {
|
||||
delete query.groupBy;
|
||||
onChange({
|
||||
...query,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rawQueryContainer}>
|
||||
<RawQuery query={templateSrv.replace(traceQlQuery)} lang={{ grammar: traceqlGrammar, name: 'traceql' }} />
|
||||
|
||||
@@ -47,7 +47,7 @@ composableKinds: DataQuery: {
|
||||
// Defines the maximum number of spans per spanset that are returned from Tempo
|
||||
spss?: int64
|
||||
filters: [...#TraceqlFilter]
|
||||
// Filters that are used to query the metrics summary
|
||||
// deprecated Filters that are used to query the metrics summary
|
||||
groupBy?: [...#TraceqlFilter]
|
||||
// The type of the table that is used to display the search results
|
||||
tableType?: #SearchTableType
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface TempoQuery extends common.DataQuery {
|
||||
exemplars?: number;
|
||||
filters: Array<TraceqlFilter>;
|
||||
/**
|
||||
* Filters that are used to query the metrics summary
|
||||
* deprecated Filters that are used to query the metrics summary
|
||||
*/
|
||||
groupBy?: Array<TraceqlFilter>;
|
||||
/**
|
||||
|
||||
@@ -172,16 +172,6 @@ describe('Tempo data source', () => {
|
||||
valueType: 'string',
|
||||
},
|
||||
],
|
||||
groupBy: [
|
||||
{
|
||||
id: 'groupBy1',
|
||||
tag: '$interpolationVar',
|
||||
},
|
||||
{
|
||||
id: 'groupBy2',
|
||||
tag: '$interpolationVar',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
let templateSrv: TemplateSrv;
|
||||
@@ -214,8 +204,6 @@ describe('Tempo data source', () => {
|
||||
expect(queries[0].filters[0].value).toBe(textWithPipe);
|
||||
expect(queries[0].filters[1].value).toBe(text);
|
||||
expect(queries[0].filters[1].tag).toBe(text);
|
||||
expect(queries[0].groupBy?.[0].tag).toBe(text);
|
||||
expect(queries[0].groupBy?.[1].tag).toBe(text);
|
||||
});
|
||||
|
||||
it('when applying template variables', async () => {
|
||||
@@ -228,8 +216,6 @@ describe('Tempo data source', () => {
|
||||
expect(resp.filters[0].value).toBe(textWithPipe);
|
||||
expect(resp.filters[1].value).toBe(scopedText);
|
||||
expect(resp.filters[1].tag).toBe(scopedText);
|
||||
expect(resp.groupBy?.[0].tag).toBe(scopedText);
|
||||
expect(resp.groupBy?.[1].tag).toBe(scopedText);
|
||||
});
|
||||
|
||||
it('when serviceMapQuery is an array', async () => {
|
||||
@@ -352,18 +338,6 @@ describe('Tempo data source', () => {
|
||||
expect(edgesFrame.meta?.preferredVisualisationType).toBe('nodeGraph');
|
||||
});
|
||||
|
||||
it('should format metrics summary query correctly', () => {
|
||||
const ds = new TempoDatasource(defaultSettings, {} as TemplateSrv);
|
||||
const queryGroupBy = [
|
||||
{ id: '1', scope: TraceqlSearchScope.Unscoped, tag: 'component' },
|
||||
{ id: '2', scope: TraceqlSearchScope.Span, tag: 'name' },
|
||||
{ id: '3', scope: TraceqlSearchScope.Resource, tag: 'service.name' },
|
||||
{ id: '4', scope: TraceqlSearchScope.Intrinsic, tag: 'kind' },
|
||||
];
|
||||
const groupBy = ds.formatGroupBy(queryGroupBy);
|
||||
expect(groupBy).toEqual('.component, span.name, resource.service.name, kind');
|
||||
});
|
||||
|
||||
describe('test the testDatasource function', () => {
|
||||
it('should return a success msg if response.ok is true', async () => {
|
||||
mockObservable = () => of({ ok: true });
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
totalsMetric,
|
||||
} from './graphTransform';
|
||||
import TempoLanguageProvider from './language_provider';
|
||||
import { createTableFrameFromMetricsSummaryQuery, emptyResponse, MetricsSummary } from './metricsSummary';
|
||||
import {
|
||||
enhanceTraceQlMetricsResponse,
|
||||
formatTraceQLResponse,
|
||||
@@ -405,19 +404,18 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
}
|
||||
|
||||
if (targets.traceqlSearch?.length) {
|
||||
try {
|
||||
if (config.featureToggles.metricsSummary) {
|
||||
const target = targets.traceqlSearch.find((t) => this.hasGroupBy(t));
|
||||
if (target) {
|
||||
const appliedQuery = this.applyVariables(target, options.scopedVars);
|
||||
const queryFromFilters = this.languageProvider.generateQueryFromFilters(appliedQuery.filters);
|
||||
subQueries.push(this.handleMetricsSummaryQuery(appliedQuery, queryFromFilters, options));
|
||||
}
|
||||
}
|
||||
if (targets.traceqlSearch[0].groupBy) {
|
||||
return of({
|
||||
error: {
|
||||
message:
|
||||
'The aggregate by query is deprecated. Please remove the current query and create a new one. Alternatively, you can use Traces Drilldown.',
|
||||
},
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
|
||||
const traceqlSearchTargets = config.featureToggles.metricsSummary
|
||||
? targets.traceqlSearch.filter((t) => !this.hasGroupBy(t))
|
||||
: targets.traceqlSearch;
|
||||
try {
|
||||
const traceqlSearchTargets = targets.traceqlSearch;
|
||||
if (traceqlSearchTargets.length > 0) {
|
||||
const appliedQuery = this.applyVariables(traceqlSearchTargets[0], options.scopedVars);
|
||||
const queryFromFilters = this.languageProvider.generateQueryFromFilters(appliedQuery.filters);
|
||||
@@ -552,17 +550,6 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
expandedQuery.filters = interpolateFilters(query.filters, scopedVars);
|
||||
}
|
||||
|
||||
if (query.groupBy) {
|
||||
expandedQuery.groupBy = query.groupBy.map((filter) => {
|
||||
const updatedFilter = {
|
||||
...filter,
|
||||
tag: this.templateSrv.replace(filter.tag ?? '', scopedVars),
|
||||
};
|
||||
|
||||
return updatedFilter;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...expandedQuery,
|
||||
query: this.templateSrv.replace(query.query ?? '', scopedVars, VariableFormatID.Pipe),
|
||||
@@ -572,22 +559,6 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
};
|
||||
}
|
||||
|
||||
formatGroupBy = (groupBy: TraceqlFilter[]) => {
|
||||
return groupBy
|
||||
?.filter((f) => f.tag)
|
||||
.map((f) => {
|
||||
if (f.scope === TraceqlSearchScope.Unscoped) {
|
||||
return `.${f.tag}`;
|
||||
}
|
||||
return f.scope !== TraceqlSearchScope.Intrinsic ? `${f.scope}.${f.tag}` : f.tag;
|
||||
})
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
hasGroupBy = (query: TempoQuery) => {
|
||||
return query.groupBy?.find((gb) => gb.tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the simplest of the queries where we have just a trace id and return trace data for it.
|
||||
* @param options
|
||||
@@ -731,93 +702,6 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
);
|
||||
}
|
||||
|
||||
handleMetricsSummaryQuery = (target: TempoQuery, query: string, options: DataQueryRequest<TempoQuery>) => {
|
||||
reportInteraction('grafana_traces_metrics_summary_queried', {
|
||||
datasourceType: 'tempo',
|
||||
app: options.app ?? '',
|
||||
grafana_version: config.buildInfo.version,
|
||||
filterCount: target.groupBy?.length ?? 0,
|
||||
});
|
||||
|
||||
if (query === '{}') {
|
||||
return of({
|
||||
error: {
|
||||
message:
|
||||
'Please ensure you do not have an empty query. This is so filters are applied and the metrics summary is not generated from all spans.',
|
||||
},
|
||||
data: emptyResponse,
|
||||
});
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const groupBy = target.groupBy ? this.formatGroupBy(target.groupBy) : '';
|
||||
return this._request('/api/metrics/summary', {
|
||||
q: query,
|
||||
groupBy,
|
||||
start: options.range.from.unix(),
|
||||
end: options.range.to.unix(),
|
||||
}).pipe(
|
||||
map((response) => {
|
||||
if (!response.data.summaries) {
|
||||
reportTempoQueryMetrics('grafana_traces_metrics_summary_response', options, {
|
||||
success: false,
|
||||
streaming: false,
|
||||
latencyMs: Math.round(performance.now() - startTime), // rounded to nearest millisecond
|
||||
query: query ?? '',
|
||||
error: getErrorMessage(`No summary data for '${groupBy}'.`),
|
||||
});
|
||||
return {
|
||||
error: {
|
||||
message: getErrorMessage(`No summary data for '${groupBy}'.`),
|
||||
},
|
||||
data: emptyResponse,
|
||||
};
|
||||
}
|
||||
// Check if any of the results have series data as older versions of Tempo placed the series data in a different structure
|
||||
const hasSeries = response.data.summaries.some((summary: MetricsSummary) => summary.series.length > 0);
|
||||
if (!hasSeries) {
|
||||
reportTempoQueryMetrics('grafana_traces_metrics_summary_response', options, {
|
||||
success: false,
|
||||
streaming: false,
|
||||
latencyMs: Math.round(performance.now() - startTime), // rounded to nearest millisecond
|
||||
query: query ?? '',
|
||||
error: getErrorMessage(`No series data. Ensure you are using an up to date version of Tempo`),
|
||||
});
|
||||
return {
|
||||
error: {
|
||||
message: getErrorMessage(`No series data. Ensure you are using an up to date version of Tempo`),
|
||||
},
|
||||
data: emptyResponse,
|
||||
};
|
||||
}
|
||||
reportTempoQueryMetrics('grafana_traces_metrics_summary_response', options, {
|
||||
success: true,
|
||||
streaming: false,
|
||||
latencyMs: Math.round(performance.now() - startTime), // rounded to nearest millisecond
|
||||
query: query ?? '',
|
||||
});
|
||||
return {
|
||||
data: createTableFrameFromMetricsSummaryQuery(response.data.summaries, query, this.instanceSettings),
|
||||
};
|
||||
}),
|
||||
catchError((error) => {
|
||||
reportTempoQueryMetrics('grafana_traces_metrics_summary_response', options, {
|
||||
success: false,
|
||||
streaming: false,
|
||||
latencyMs: Math.round(performance.now() - startTime), // rounded to nearest millisecond
|
||||
query: query ?? '',
|
||||
error: getErrorMessage(error.data.message),
|
||||
statusCode: error.status,
|
||||
statusText: error.statusText,
|
||||
});
|
||||
return of({
|
||||
error: { message: getErrorMessage(error.data.message) },
|
||||
data: emptyResponse,
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// This function can probably be simplified by avoiding passing both `targets` and `query`,
|
||||
// since `query` is built from `targets`, if you look at how this function is currently called
|
||||
handleStreamingQuery(
|
||||
|
||||
@@ -8,38 +8,6 @@ import { intrinsics } from './traceql/traceql';
|
||||
import { Scope } from './types';
|
||||
|
||||
describe('Language_provider', () => {
|
||||
describe('should get correct metrics summary tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
const lp = setup(v1Tags);
|
||||
const tags = lp.getMetricsSummaryTags();
|
||||
expect(tags).toEqual(['bar', 'foo']);
|
||||
});
|
||||
|
||||
it('for API v2 intrinsic tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const tags = lp.getMetricsSummaryTags(TraceqlSearchScope.Intrinsic);
|
||||
expect(tags).toEqual(uniq(['duration', 'kind', 'name', 'status'].concat(intrinsics)));
|
||||
});
|
||||
|
||||
it('for API v2 resource tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const tags = lp.getMetricsSummaryTags(TraceqlSearchScope.Resource);
|
||||
expect(tags).toEqual(['cluster', 'container']);
|
||||
});
|
||||
|
||||
it('for API v2 span tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const tags = lp.getMetricsSummaryTags(TraceqlSearchScope.Span);
|
||||
expect(tags).toEqual(['db']);
|
||||
});
|
||||
|
||||
it('for API v2 unscoped tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const tags = lp.getMetricsSummaryTags(TraceqlSearchScope.Unscoped);
|
||||
expect(tags).toEqual(['cluster', 'container', 'db']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get correct tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
const lp = setup(v1Tags);
|
||||
|
||||
@@ -97,18 +97,6 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
return [];
|
||||
};
|
||||
|
||||
getMetricsSummaryTags = (scope?: TraceqlSearchScope) => {
|
||||
if (this.tagsV2 && scope) {
|
||||
if (scope === TraceqlSearchScope.Unscoped) {
|
||||
return getUnscopedTags(this.tagsV2);
|
||||
}
|
||||
return getTagsByScope(this.tagsV2, scope);
|
||||
} else if (this.tagsV1) {
|
||||
return this.tagsV1;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
getTraceqlAutocompleteTags = (scope?: string) => {
|
||||
if (this.tagsV2) {
|
||||
if (!scope) {
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import { defaultSettings } from './datasource.test';
|
||||
import {
|
||||
createTableFrameFromMetricsSummaryQuery,
|
||||
emptyResponse,
|
||||
getConfigQuery,
|
||||
transformToMetricsData,
|
||||
} from './metricsSummary';
|
||||
|
||||
describe('MetricsSummary', () => {
|
||||
describe('createTableFrameFromMetricsSummaryQuery', () => {
|
||||
it('should return emptyResponse when state is LoadingState.Error', () => {
|
||||
const result = createTableFrameFromMetricsSummaryQuery([], '', defaultSettings);
|
||||
expect(result).toEqual(emptyResponse);
|
||||
});
|
||||
|
||||
it('should return correctly when state is LoadingState.Done', () => {
|
||||
const data = [
|
||||
{
|
||||
spanCount: '10',
|
||||
errorSpanCount: '1',
|
||||
p50: '1',
|
||||
p90: '2',
|
||||
p95: '3',
|
||||
p99: '4',
|
||||
series: [
|
||||
{
|
||||
key: 'span.http.status_code',
|
||||
value: {
|
||||
type: 3,
|
||||
n: 208,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
value: {
|
||||
type: 4,
|
||||
f: 38.1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = createTableFrameFromMetricsSummaryQuery(
|
||||
data,
|
||||
'{name="HTTP POST - post"} | by(resource.service.name)',
|
||||
defaultSettings
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"config": {
|
||||
"displayNameFromDS": "span.http.status_code",
|
||||
"links": [
|
||||
{
|
||||
"internal": {
|
||||
"datasourceName": "tempo",
|
||||
"datasourceUid": "gdev-tempo",
|
||||
"query": {
|
||||
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)",
|
||||
"queryType": "traceql",
|
||||
},
|
||||
},
|
||||
"title": "Query in explore",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"noValue": "<no value>",
|
||||
},
|
||||
"name": "span.http.status_code",
|
||||
"type": "string",
|
||||
"values": [
|
||||
208,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"displayNameFromDS": "temperature",
|
||||
"links": [
|
||||
{
|
||||
"internal": {
|
||||
"datasourceName": "tempo",
|
||||
"datasourceUid": "gdev-tempo",
|
||||
"query": {
|
||||
"query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)",
|
||||
"queryType": "traceql",
|
||||
},
|
||||
},
|
||||
"title": "Query in explore",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"noValue": "<no value>",
|
||||
},
|
||||
"name": "temperature",
|
||||
"type": "string",
|
||||
"values": [
|
||||
38.1,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "Span count",
|
||||
},
|
||||
"name": "spanCount",
|
||||
"type": "number",
|
||||
"values": [
|
||||
10,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "Error",
|
||||
"unit": "percent",
|
||||
},
|
||||
"name": "errorPercentage",
|
||||
"type": "number",
|
||||
"values": [
|
||||
10,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "p50",
|
||||
"unit": "ns",
|
||||
},
|
||||
"name": "p50",
|
||||
"type": "number",
|
||||
"values": [
|
||||
1,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "p90",
|
||||
"unit": "ns",
|
||||
},
|
||||
"name": "p90",
|
||||
"type": "number",
|
||||
"values": [
|
||||
2,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "p95",
|
||||
"unit": "ns",
|
||||
},
|
||||
"name": "p95",
|
||||
"type": "number",
|
||||
"values": [
|
||||
3,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"custom": {
|
||||
"width": 150,
|
||||
},
|
||||
"displayNameFromDS": "p99",
|
||||
"unit": "ns",
|
||||
},
|
||||
"name": "p99",
|
||||
"type": "number",
|
||||
"values": [
|
||||
4,
|
||||
],
|
||||
},
|
||||
],
|
||||
"length": 1,
|
||||
"meta": {
|
||||
"preferredVisualisationType": "table",
|
||||
},
|
||||
"name": "Metrics Summary",
|
||||
"refId": "metrics-summary",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('transformToMetricsData should return correctly', () => {
|
||||
const data = {
|
||||
spanCount: '10',
|
||||
errorSpanCount: '1',
|
||||
p50: '1',
|
||||
p90: '2',
|
||||
p95: '3',
|
||||
p99: '4',
|
||||
series,
|
||||
};
|
||||
const result = transformToMetricsData(data);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"contains_sink": "true",
|
||||
"errorPercentage": 10,
|
||||
"p50": 1,
|
||||
"p90": 2,
|
||||
"p95": 3,
|
||||
"p99": 4,
|
||||
"room": "kitchen",
|
||||
"span.http.status_code": 208,
|
||||
"spanCount": 10,
|
||||
"spanKind": "server",
|
||||
"spanStatus": "ok",
|
||||
"temperature": 38.1,
|
||||
"window_open": "8h",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('getConfigQuery should return correctly for empty target query', () => {
|
||||
const result = getConfigQuery(series, '{}');
|
||||
expect(result).toEqual(
|
||||
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}}'
|
||||
);
|
||||
});
|
||||
|
||||
it('getConfigQuery should return correctly for target query', () => {
|
||||
const result = getConfigQuery(series, '{name="HTTP POST - post"} | by(resource.service.name)');
|
||||
expect(result).toEqual(
|
||||
'{name="HTTP POST - post" && span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)'
|
||||
);
|
||||
});
|
||||
|
||||
it('getConfigQuery should return correctly for target query without brackets', () => {
|
||||
const result = getConfigQuery(series, 'by(resource.service.name)');
|
||||
expect(result).toEqual(
|
||||
'{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const series = [
|
||||
{
|
||||
key: 'span.http.status_code',
|
||||
value: {
|
||||
type: 3,
|
||||
n: 208,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
value: {
|
||||
type: 4,
|
||||
f: 38.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'room',
|
||||
value: {
|
||||
type: 5,
|
||||
s: 'kitchen',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contains_sink',
|
||||
value: {
|
||||
type: 6,
|
||||
b: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'window_open',
|
||||
value: {
|
||||
type: 7,
|
||||
d: '8h',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanStatus',
|
||||
value: {
|
||||
type: 8,
|
||||
status: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanKind',
|
||||
value: {
|
||||
type: 9,
|
||||
kind: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,273 +0,0 @@
|
||||
import {
|
||||
createDataFrame,
|
||||
DataSourceInstanceSettings,
|
||||
FieldDTO,
|
||||
FieldType,
|
||||
MutableDataFrame,
|
||||
sortDataFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
export type MetricsSummary = {
|
||||
spanCount: string;
|
||||
errorSpanCount?: string;
|
||||
p50: string;
|
||||
p90: string;
|
||||
p95: string;
|
||||
p99: string;
|
||||
series: Series[];
|
||||
};
|
||||
|
||||
type Series = {
|
||||
key: string;
|
||||
value: {
|
||||
type: number;
|
||||
n?: number;
|
||||
f?: number;
|
||||
s?: string;
|
||||
b?: string;
|
||||
d?: string;
|
||||
status?: number;
|
||||
kind?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type MetricsData = {
|
||||
spanCount: number;
|
||||
errorPercentage: number | string;
|
||||
p50: number;
|
||||
p90: number;
|
||||
p95: number;
|
||||
p99: number;
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
export function createTableFrameFromMetricsSummaryQuery(
|
||||
data: MetricsSummary[],
|
||||
targetQuery: string,
|
||||
instanceSettings: DataSourceInstanceSettings
|
||||
) {
|
||||
let frame;
|
||||
|
||||
if (!data.length) {
|
||||
return emptyResponse;
|
||||
}
|
||||
|
||||
const dynamicMetrics: Record<string, FieldDTO> = {};
|
||||
data.forEach((res: MetricsSummary) => {
|
||||
const configQuery = getConfigQuery(res.series, targetQuery);
|
||||
res.series.forEach((series: Series) => {
|
||||
dynamicMetrics[series.key] = {
|
||||
name: `${series.key}`,
|
||||
type: FieldType.string,
|
||||
config: getConfig(series, configQuery, instanceSettings),
|
||||
values: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
frame = createDataFrame({
|
||||
name: 'Metrics Summary',
|
||||
refId: 'metrics-summary',
|
||||
fields: [
|
||||
...Object.values(dynamicMetrics).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
{
|
||||
name: 'spanCount',
|
||||
type: FieldType.number,
|
||||
config: { displayNameFromDS: 'Span count', custom: { width: 150 } },
|
||||
},
|
||||
{
|
||||
name: 'errorPercentage',
|
||||
type: FieldType.number,
|
||||
config: { displayNameFromDS: 'Error', unit: 'percent', custom: { width: 150 } },
|
||||
},
|
||||
getPercentileRow('p50'),
|
||||
getPercentileRow('p90'),
|
||||
getPercentileRow('p95'),
|
||||
getPercentileRow('p99'),
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'table',
|
||||
},
|
||||
});
|
||||
|
||||
const metricsData = data.map(transformToMetricsData);
|
||||
frame.length = metricsData.length;
|
||||
for (const trace of metricsData) {
|
||||
for (const field of frame.fields) {
|
||||
field.values.push(trace[field.name]);
|
||||
}
|
||||
}
|
||||
frame = sortDataFrame(frame, 0);
|
||||
|
||||
return [frame];
|
||||
}
|
||||
|
||||
export const transformToMetricsData = (data: MetricsSummary) => {
|
||||
const errorPercentage = data.errorSpanCount
|
||||
? (getNumberForMetric(data.errorSpanCount) / getNumberForMetric(data.spanCount)) * 100
|
||||
: '0%';
|
||||
|
||||
const metricsData: MetricsData = {
|
||||
spanCount: getNumberForMetric(data.spanCount),
|
||||
errorPercentage,
|
||||
p50: getNumberForMetric(data.p50),
|
||||
p90: getNumberForMetric(data.p90),
|
||||
p95: getNumberForMetric(data.p95),
|
||||
p99: getNumberForMetric(data.p99),
|
||||
};
|
||||
|
||||
data.series.forEach((series: Series) => {
|
||||
metricsData[`${series.key}`] = getMetricValue(series) || '';
|
||||
});
|
||||
|
||||
return metricsData;
|
||||
};
|
||||
|
||||
export const getConfigQuery = (series: Series[], targetQuery: string) => {
|
||||
const queryParts = series.map((x: Series) => {
|
||||
const isNumber = x.value.type === 3 || x.value.type === 4;
|
||||
const isIntrinsic = x.value.type === 8 || x.value.type === 9;
|
||||
const surround = isNumber || isIntrinsic ? '' : '"';
|
||||
return `${x.key}=${surround}` + '${__data.fields["' + x.key + '"]}' + `${surround}`;
|
||||
});
|
||||
|
||||
let configQuery = '';
|
||||
const closingBracketIndex = targetQuery.indexOf('}');
|
||||
|
||||
if (closingBracketIndex !== -1) {
|
||||
const queryAfterClosingBracket = targetQuery.substring(closingBracketIndex + 1);
|
||||
configQuery = targetQuery.substring(0, closingBracketIndex);
|
||||
if (queryParts.length > 0) {
|
||||
configQuery += targetQuery.replace(/\s/g, '').includes('{}') ? '' : ' && ';
|
||||
configQuery += `${queryParts.join(' && ')}`;
|
||||
configQuery += `}`;
|
||||
}
|
||||
configQuery += `${queryAfterClosingBracket}`;
|
||||
} else {
|
||||
configQuery = `{${queryParts.join(' && ')}} | ${targetQuery}`;
|
||||
}
|
||||
|
||||
return configQuery;
|
||||
};
|
||||
|
||||
const getConfig = (series: Series, query: string, instanceSettings: DataSourceInstanceSettings) => {
|
||||
const commonConfig = {
|
||||
displayNameFromDS: series.key,
|
||||
noValue: '<no value>',
|
||||
links: [
|
||||
{
|
||||
title: 'Query in explore',
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: instanceSettings.uid,
|
||||
datasourceName: instanceSettings.name,
|
||||
query: {
|
||||
query,
|
||||
queryType: 'traceql',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (series.value.type === 7) {
|
||||
return {
|
||||
...commonConfig,
|
||||
unit: 'ns',
|
||||
};
|
||||
}
|
||||
return { ...commonConfig };
|
||||
};
|
||||
|
||||
const NO_VALUE = '';
|
||||
|
||||
const getMetricValue = (series: Series) => {
|
||||
if (!series.value.type) {
|
||||
return NO_VALUE;
|
||||
}
|
||||
|
||||
switch (series.value.type) {
|
||||
case 3:
|
||||
return series.value.n;
|
||||
case 4:
|
||||
return series.value.f;
|
||||
case 5:
|
||||
return series.value.s;
|
||||
case 6:
|
||||
return series.value.b;
|
||||
case 7:
|
||||
return series.value.d;
|
||||
case 8:
|
||||
return getSpanStatusCode(series.value.status);
|
||||
case 9:
|
||||
return getSpanKind(series.value.kind);
|
||||
default:
|
||||
return NO_VALUE;
|
||||
}
|
||||
};
|
||||
|
||||
// Values set according to Tempo enum: https://github.com/grafana/tempo/blob/main/pkg/traceql/enum_statics.go
|
||||
const getSpanStatusCode = (statusCode: number | undefined) => {
|
||||
if (!statusCode) {
|
||||
return NO_VALUE;
|
||||
}
|
||||
|
||||
switch (statusCode) {
|
||||
case 0:
|
||||
return 'error';
|
||||
case 1:
|
||||
return 'ok';
|
||||
default:
|
||||
return 'unset';
|
||||
}
|
||||
};
|
||||
|
||||
// Values set according to Tempo enum: https://github.com/grafana/tempo/blob/main/pkg/traceql/enum_statics.go
|
||||
const getSpanKind = (kind: number | undefined) => {
|
||||
if (!kind) {
|
||||
return NO_VALUE;
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case 1:
|
||||
return 'internal';
|
||||
case 2:
|
||||
return 'client';
|
||||
case 3:
|
||||
return 'server';
|
||||
case 4:
|
||||
return 'producer';
|
||||
case 5:
|
||||
return 'consumer';
|
||||
default:
|
||||
return 'unspecified';
|
||||
}
|
||||
};
|
||||
|
||||
const getPercentileRow = (name: string) => {
|
||||
return {
|
||||
name: name,
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
displayNameFromDS: name,
|
||||
unit: 'ns',
|
||||
custom: {
|
||||
width: 150,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getNumberForMetric = (metric: string) => {
|
||||
const number = parseInt(metric, 10);
|
||||
return isNaN(number) ? 0 : number;
|
||||
};
|
||||
|
||||
export const emptyResponse = new MutableDataFrame({
|
||||
name: 'Metrics Summary',
|
||||
refId: 'metrics-summary',
|
||||
fields: [],
|
||||
meta: {
|
||||
preferredVisualisationType: 'table',
|
||||
},
|
||||
});
|
||||
@@ -91,7 +91,6 @@ export const migrateFromSearchToTraceQLSearch = (query: TempoQuery) => {
|
||||
const migratedQuery: TempoQuery = {
|
||||
datasource: query.datasource,
|
||||
filters,
|
||||
groupBy: query.groupBy,
|
||||
limit: query.limit,
|
||||
query: query.query,
|
||||
queryType: 'traceqlSearch',
|
||||
|
||||
Reference in New Issue
Block a user