The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/zipkin/QueryField.tsx

330 lines
9.9 KiB

import { css } from '@emotion/css';
import { fromPairs } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useAsyncFn, useMount, useMountedState } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import {
ButtonCascader,
CascaderOption,
FileDropzone,
InlineField,
InlineFieldRow,
RadioButtonGroup,
useTheme2,
QueryField,
useStyles2,
Modal,
HorizontalGroup,
Button,
} from '@grafana/ui';
import { apiPrefix } from './constants';
import { ZipkinDatasource } from './datasource';
import { ZipkinQuery, ZipkinQueryType, ZipkinSpan } from './types';
type Props = QueryEditorProps<ZipkinDatasource, ZipkinQuery>;
const getStyles = (theme: GrafanaTheme2) => {
return {
tracesCascader: css({
label: 'tracesCascader',
marginRight: theme.spacing(1),
}),
};
};
export const ZipkinQueryField = ({ query, onChange, onRunQuery, datasource }: Props) => {
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [alertText, setAlertText] = useState('');
const serviceOptions = useServices(datasource, setAlertText);
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { onLoadOptions, allOptions } = useLoadOptions(datasource, setAlertText);
const onSelectTrace = useCallback(
(values: string[], selectedOptions: CascaderOption[]) => {
if (selectedOptions.length === 3) {
const traceID = selectedOptions[2].value;
onChange({ ...query, query: traceID });
onRunQuery();
}
},
[onChange, onRunQuery, query]
);
useEffect(() => {
if (!query.queryType) {
onChange({
...query,
queryType: 'traceID',
});
}
}, [query, onChange]);
const onChangeQuery = (value: string) => {
const nextQuery = { ...query, query: value };
onChange(nextQuery);
};
let cascaderOptions = useMapToCascaderOptions(serviceOptions, allOptions);
return (
<>
<Modal title={'Upload trace'} isOpen={uploadModalOpen} onDismiss={() => setUploadModalOpen(false)}>
<div className={css({ padding: theme.spacing(2) })}>
<FileDropzone
options={{ multiple: false }}
onLoad={(result) => {
datasource.uploadedJson = result;
onChange({
...query,
queryType: 'upload',
});
setUploadModalOpen(false);
onRunQuery();
}}
/>
</div>
</Modal>
<InlineFieldRow>
<InlineField label="Query type" grow={true}>
<HorizontalGroup spacing={'sm'} align={'center'} justify={'space-between'}>
<RadioButtonGroup<ZipkinQueryType>
options={[{ value: 'traceID', label: 'TraceID' }]}
value={query.queryType || 'traceID'}
onChange={(v) =>
onChange({
...query,
queryType: v,
})
}
size="md"
/>
<Button
variant="secondary"
size="sm"
onClick={() => {
setUploadModalOpen(true);
}}
>
Import trace
</Button>
</HorizontalGroup>
</InlineField>
</InlineFieldRow>
{query.queryType === 'traceID' && (
<InlineFieldRow>
<ButtonCascader
options={cascaderOptions}
onChange={onSelectTrace}
loadData={onLoadOptions}
variant="secondary"
buttonProps={{ className: styles.tracesCascader }}
>
Traces
</ButtonCascader>
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
<QueryField
query={query.query}
onChange={onChangeQuery}
onRunQuery={onRunQuery}
placeholder={'Insert Trace ID (run with Shift+Enter)'}
portalOrigin="zipkin"
/>
</div>
</InlineFieldRow>
)}
{alertText && <TemporaryAlert text={alertText} severity={'error'} />}
</>
);
};
// Exported for tests
export function useServices(
datasource: ZipkinDatasource,
setErrorText: (text: string) => void
): AsyncState<CascaderOption[]> {
const url = `${apiPrefix}/services`;
const [servicesOptions, fetch] = useAsyncFn(async (): Promise<CascaderOption[]> => {
try {
const services: string[] | null = await datasource.metadataRequest(url);
if (services) {
return services.sort().map((service) => ({
label: service,
value: service,
isLeaf: false,
}));
}
return [];
} catch (error) {
const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
const errorText = `Failed to load spans from Zipkin: ${errorToShow.toString()}`;
setErrorText(errorText);
throw error;
}
}, [datasource]);
useMount(() => {
// We should probably call this periodically to get new services after mount.
fetch();
});
return servicesOptions;
}
type OptionsState = {
[serviceName: string]: {
[spanName: string]: {
[traceId: string]: string;
};
};
};
// Exported for tests
export function useLoadOptions(datasource: ZipkinDatasource, setErrorText: (text: string) => void) {
const isMounted = useMountedState();
const [allOptions, setAllOptions] = useState<OptionsState>({});
const [, fetchSpans] = useAsyncFn(
async function findSpans(service: string): Promise<void> {
const url = `${apiPrefix}/spans`;
try {
// The response of this should have been full ZipkinSpan objects based on API docs but is just list
// of span names.
// TODO: check if this is some issue of version used or something else
const response: string[] = await datasource.metadataRequest(url, { serviceName: service });
if (isMounted()) {
setAllOptions((state) => {
const spanOptions = fromPairs(response.map((span: string) => [span, undefined]));
return {
...state,
[service]: spanOptions as any,
};
});
}
} catch (error) {
const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
const errorText = `Failed to load spans from Zipkin: ${errorToShow.toString()}`;
setErrorText(errorText);
throw error;
}
},
[datasource, allOptions]
);
const [, fetchTraces] = useAsyncFn(
async function findTraces(serviceName: string, spanName: string): Promise<void> {
const url = `${apiPrefix}/traces`;
const search = {
serviceName,
spanName,
// See other params and default here https://zipkin.io/zipkin-api/#/default/get_traces
};
try {
// This should return just root traces as there isn't any nesting
const traces: ZipkinSpan[][] = await datasource.metadataRequest(url, search);
if (isMounted()) {
const newTraces = traces.length
? fromPairs(
traces.map((trace) => {
const rootSpan = trace.find((span) => !span.parentId)!;
return [`${rootSpan.name} [${Math.floor(rootSpan.duration / 1000)} ms]`, rootSpan.traceId];
})
)
: noTracesOptions;
setAllOptions((state) => {
const spans = state[serviceName];
return {
...state,
[serviceName]: {
...spans,
[spanName]: newTraces,
},
};
});
}
} catch (error) {
const errorToShow = error instanceof Error ? error : 'An unknown error occurred';
const errorText = `Failed to load spans from Zipkin: ${errorToShow.toString()}`;
setErrorText(errorText);
throw error;
}
},
[datasource]
);
const onLoadOptions = useCallback(
(selectedOptions: CascaderOption[]) => {
const service = selectedOptions[0].value;
if (selectedOptions.length === 1) {
fetchSpans(service);
} else if (selectedOptions.length === 2) {
const spanName = selectedOptions[1].value;
fetchTraces(service, spanName);
}
},
[fetchSpans, fetchTraces]
);
return {
onLoadOptions,
allOptions,
};
}
function useMapToCascaderOptions(services: AsyncState<CascaderOption[]>, allOptions: OptionsState) {
return useMemo(() => {
let cascaderOptions: CascaderOption[] = [];
if (services.value && services.value.length) {
cascaderOptions = services.value.map((services) => {
return {
...services,
children:
allOptions[services.value] &&
Object.keys(allOptions[services.value]).map((spanName) => {
return {
label: spanName,
value: spanName,
isLeaf: false,
children:
allOptions[services.value][spanName] &&
Object.keys(allOptions[services.value][spanName]).map((traceName) => {
return {
label: traceName,
value: allOptions[services.value][spanName][traceName],
};
}),
};
}),
};
});
} else if (services.value && !services.value.length) {
cascaderOptions = noTracesFoundOptions;
}
return cascaderOptions;
}, [services, allOptions]);
}
const NO_TRACES_KEY = '__NO_TRACES__';
const noTracesFoundOptions = [
{
label: 'No traces found',
value: 'no_traces',
isLeaf: true,
// Cannot be disabled because then cascader shows 'loading' for some reason.
// disabled: true,
},
];
const noTracesOptions = {
'[No traces in time range]': NO_TRACES_KEY,
};