mirror of https://github.com/grafana/grafana
Loki: Show label options for unwrap operation (#52810)
* Loki: Show options for unwrap operation * Add comment for checkpull/52858/head
parent
33f67ed6e2
commit
3f681114e5
@ -0,0 +1,20 @@ |
|||||||
|
import { isBytesString } from './language_utils'; |
||||||
|
|
||||||
|
describe('isBytesString', () => { |
||||||
|
it('correctly matches bytes string with integers', () => { |
||||||
|
expect(isBytesString('500b')).toBe(true); |
||||||
|
expect(isBytesString('2TB')).toBe(true); |
||||||
|
}); |
||||||
|
it('correctly matches bytes string with float', () => { |
||||||
|
expect(isBytesString('500.4kib')).toBe(true); |
||||||
|
expect(isBytesString('10.4654Mib')).toBe(true); |
||||||
|
}); |
||||||
|
it('does not match integer without unit', () => { |
||||||
|
expect(isBytesString('500')).toBe(false); |
||||||
|
expect(isBytesString('10')).toBe(false); |
||||||
|
}); |
||||||
|
it('does not match float without unit', () => { |
||||||
|
expect(isBytesString('50.047')).toBe(false); |
||||||
|
expect(isBytesString('1.234')).toBe(false); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,147 @@ |
|||||||
|
import { screen, render } from '@testing-library/react'; |
||||||
|
import userEvent from '@testing-library/user-event'; |
||||||
|
import React, { ComponentProps } from 'react'; |
||||||
|
|
||||||
|
import { DataFrame, DataSourceApi, DataSourcePluginMeta, FieldType, toDataFrame } from '@grafana/data'; |
||||||
|
import { |
||||||
|
QueryBuilderOperation, |
||||||
|
QueryBuilderOperationParamDef, |
||||||
|
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; |
||||||
|
|
||||||
|
import { LokiDatasource } from '../../datasource'; |
||||||
|
import { LokiOperationId } from '../types'; |
||||||
|
|
||||||
|
import { UnwrapParamEditor } from './UnwrapParamEditor'; |
||||||
|
|
||||||
|
describe('UnwrapParamEditor', () => { |
||||||
|
it('shows value if value present', () => { |
||||||
|
const props = createProps({ value: 'unique' }); |
||||||
|
render(<UnwrapParamEditor {...props} />); |
||||||
|
expect(screen.getByText('unique')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows no label options if no samples are returned', async () => { |
||||||
|
const props = createProps(); |
||||||
|
render(<UnwrapParamEditor {...props} />); |
||||||
|
const input = screen.getByRole('combobox'); |
||||||
|
await userEvent.click(input); |
||||||
|
expect(screen.getByText('No labels found')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows no label options for non-metric query', async () => { |
||||||
|
const props = createProps({ |
||||||
|
query: { |
||||||
|
labels: [{ op: '=', label: 'foo', value: 'bar' }], |
||||||
|
operations: [ |
||||||
|
{ id: LokiOperationId.Logfmt, params: [] }, |
||||||
|
{ id: LokiOperationId.Unwrap, params: ['', ''] }, |
||||||
|
], |
||||||
|
}, |
||||||
|
}); |
||||||
|
render(<UnwrapParamEditor {...props} />); |
||||||
|
const input = screen.getByRole('combobox'); |
||||||
|
await userEvent.click(input); |
||||||
|
expect(screen.getByText('No labels found')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows labels with unwrap-friendly values', async () => { |
||||||
|
const props = createProps({}, frames); |
||||||
|
render(<UnwrapParamEditor {...props} />); |
||||||
|
const input = screen.getByRole('combobox'); |
||||||
|
await userEvent.click(input); |
||||||
|
expect(await screen.findByText('status')).toBeInTheDocument(); |
||||||
|
expect(await screen.findByText('duration')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
const createProps = ( |
||||||
|
propsOverrides?: Partial<ComponentProps<typeof UnwrapParamEditor>>, |
||||||
|
mockedSample?: DataFrame[] |
||||||
|
) => { |
||||||
|
const propsDefault = { |
||||||
|
value: undefined, |
||||||
|
onChange: jest.fn(), |
||||||
|
onRunQuery: jest.fn(), |
||||||
|
index: 1, |
||||||
|
operationIndex: 1, |
||||||
|
query: { |
||||||
|
labels: [{ op: '=', label: 'foo', value: 'bar' }], |
||||||
|
operations: [ |
||||||
|
{ id: LokiOperationId.Logfmt, params: [] }, |
||||||
|
{ id: LokiOperationId.Unwrap, params: ['', ''] }, |
||||||
|
{ id: LokiOperationId.SumOverTime, params: ['5m'] }, |
||||||
|
{ id: '__sum_by', params: ['job'] }, |
||||||
|
], |
||||||
|
}, |
||||||
|
paramDef: {} as QueryBuilderOperationParamDef, |
||||||
|
operation: {} as QueryBuilderOperation, |
||||||
|
datasource: new LokiDatasource( |
||||||
|
{ |
||||||
|
id: 1, |
||||||
|
uid: '', |
||||||
|
type: 'loki', |
||||||
|
name: 'loki-test', |
||||||
|
access: 'proxy', |
||||||
|
url: '', |
||||||
|
jsonData: {}, |
||||||
|
meta: {} as DataSourcePluginMeta, |
||||||
|
}, |
||||||
|
undefined, |
||||||
|
undefined |
||||||
|
) as DataSourceApi, |
||||||
|
}; |
||||||
|
const props = { ...propsDefault, ...propsOverrides }; |
||||||
|
|
||||||
|
if (props.datasource instanceof LokiDatasource) { |
||||||
|
const resolvedValue = mockedSample ?? []; |
||||||
|
props.datasource.getDataSamples = jest.fn().mockResolvedValue(resolvedValue); |
||||||
|
} |
||||||
|
return props; |
||||||
|
}; |
||||||
|
|
||||||
|
const frames = [ |
||||||
|
toDataFrame({ |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'labels', |
||||||
|
type: FieldType.other, |
||||||
|
values: [ |
||||||
|
{ |
||||||
|
compose_project: 'docker-compose', |
||||||
|
compose_service: 'app', |
||||||
|
container_name: 'docker-compose_app_1', |
||||||
|
duration: '2.807709ms', |
||||||
|
filename: '/var/log/docker/37c87fe98cbfa28327c1de10c4aff72c58154d8e4d129118ff2024692360b677/json.log', |
||||||
|
host: 'docker-desktop', |
||||||
|
instance: 'docker-compose_app_1', |
||||||
|
job: 'tns/app', |
||||||
|
level: 'info', |
||||||
|
msg: 'HTTP client success', |
||||||
|
namespace: 'tns', |
||||||
|
source: 'stdout', |
||||||
|
status: '200', |
||||||
|
traceID: '6a3d34c4225776f6', |
||||||
|
url: 'http://db', |
||||||
|
}, |
||||||
|
{ |
||||||
|
compose_project: 'docker-compose', |
||||||
|
compose_service: 'app', |
||||||
|
container_name: 'docker-compose_app_1', |
||||||
|
duration: '7.432542ms', |
||||||
|
filename: '/var/log/docker/37c87fe98cbfa28327c1de10c4aff72c58154d8e4d129118ff2024692360b677/json.log', |
||||||
|
host: 'docker-desktop', |
||||||
|
instance: 'docker-compose_app_1', |
||||||
|
job: 'tns/app', |
||||||
|
level: 'info', |
||||||
|
msg: 'HTTP client success', |
||||||
|
namespace: 'tns', |
||||||
|
source: 'stdout', |
||||||
|
status: '200', |
||||||
|
traceID: '18e99189831471f6', |
||||||
|
url: 'http://db', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}), |
||||||
|
]; |
@ -0,0 +1,96 @@ |
|||||||
|
import { isNaN } from 'lodash'; |
||||||
|
import React, { useState } from 'react'; |
||||||
|
|
||||||
|
import { isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; |
||||||
|
import { Select } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { getOperationParamId } from '../../../prometheus/querybuilder/shared/operationUtils'; |
||||||
|
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types'; |
||||||
|
import { LokiDatasource } from '../../datasource'; |
||||||
|
import { isBytesString } from '../../language_utils'; |
||||||
|
import { getLogQueryFromMetricsQuery, isValidQuery } from '../../query_utils'; |
||||||
|
import { lokiQueryModeller } from '../LokiQueryModeller'; |
||||||
|
import { LokiVisualQuery } from '../types'; |
||||||
|
|
||||||
|
export function UnwrapParamEditor({ |
||||||
|
onChange, |
||||||
|
index, |
||||||
|
operationIndex, |
||||||
|
value, |
||||||
|
query, |
||||||
|
datasource, |
||||||
|
}: QueryBuilderOperationParamEditorProps) { |
||||||
|
const [state, setState] = useState<{ |
||||||
|
options?: Array<SelectableValue<string>>; |
||||||
|
isLoading?: boolean; |
||||||
|
}>({}); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Select |
||||||
|
inputId={getOperationParamId(operationIndex, index)} |
||||||
|
onOpenMenu={async () => { |
||||||
|
// This check is always true, we do it to make typescript happy
|
||||||
|
if (datasource instanceof LokiDatasource) { |
||||||
|
setState({ isLoading: true }); |
||||||
|
const options = await loadUnwrapOptions(query, datasource); |
||||||
|
setState({ options, isLoading: undefined }); |
||||||
|
} |
||||||
|
}} |
||||||
|
isLoading={state.isLoading} |
||||||
|
allowCustomValue |
||||||
|
noOptionsMessage="No labels found" |
||||||
|
loadingMessage="Loading labels" |
||||||
|
options={state.options} |
||||||
|
value={value ? toOption(value.toString()) : null} |
||||||
|
onChange={(value) => { |
||||||
|
if (value.value) { |
||||||
|
onChange(index, value.value); |
||||||
|
} |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
async function loadUnwrapOptions( |
||||||
|
query: LokiVisualQuery, |
||||||
|
datasource: LokiDatasource |
||||||
|
): Promise<Array<SelectableValue<string>>> { |
||||||
|
const queryExpr = lokiQueryModeller.renderQuery(query); |
||||||
|
const logExpr = getLogQueryFromMetricsQuery(queryExpr); |
||||||
|
if (!isValidQuery(logExpr)) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
const samples = await datasource.getDataSamples({ expr: logExpr, refId: 'unwrap_samples' }); |
||||||
|
const labelsArray: Array<{ [key: string]: string }> | undefined = |
||||||
|
samples[0]?.fields?.find((field) => field.name === 'labels')?.values.toArray() ?? []; |
||||||
|
|
||||||
|
if (!labelsArray || labelsArray.length === 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
// We do this only for first label object, because we want to consider only labels that are present in all log lines
|
||||||
|
// possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value
|
||||||
|
const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => { |
||||||
|
const value = labelsArray[0][key]; |
||||||
|
if (!value) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value); |
||||||
|
}); |
||||||
|
|
||||||
|
const unwrapLabels: string[] = []; |
||||||
|
for (const label of possibleUnwrapLabels) { |
||||||
|
// Add only labels that are present in every line to unwrapLabels
|
||||||
|
if (labelsArray.every((obj) => obj[label])) { |
||||||
|
unwrapLabels.push(label); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const labelOptions = unwrapLabels.map((label) => ({ |
||||||
|
label, |
||||||
|
value: label, |
||||||
|
})); |
||||||
|
|
||||||
|
return labelOptions; |
||||||
|
} |
Loading…
Reference in new issue