import { ExploreQueryFieldProps } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { ButtonCascader, CascaderOption } from '@grafana/ui'; import React from 'react'; import { JaegerDatasource, JaegerQuery } from './datasource'; import { Span, TraceResponse } from './types'; import { dispatch } from 'app/store/store'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; const ALL_OPERATIONS_KEY = '__ALL__'; const NO_TRACES_KEY = '__NO_TRACES__'; type Props = ExploreQueryFieldProps; interface State { serviceOptions: CascaderOption[]; } function findRootSpan(spans: Span[]): Span | undefined { return spans.find((s) => !s.references?.length); } function getLabelFromTrace(trace: TraceResponse): string { const rootSpan = findRootSpan(trace.spans); if (rootSpan) { return `${rootSpan.operationName} [${rootSpan.duration / 1000} ms]`; } return trace.traceID; } export class JaegerQueryField extends React.PureComponent { private _isMounted: boolean; constructor(props: Props, context: React.Context) { super(props, context); this.state = { serviceOptions: [], }; } componentDidMount() { this._isMounted = true; // We should probably call this periodically to get new services after mount. this.getServices(); } componentWillUnmount(): void { this._isMounted = false; } async getServices() { const url = '/api/services'; const { datasource } = this.props; try { const services: string[] | null = await datasource.metadataRequest(url); if (!this._isMounted) { return; } if (services) { const serviceOptions: CascaderOption[] = services.sort().map((service) => ({ label: service, value: service, isLeaf: false, })); this.setState({ serviceOptions }); } } catch (error) { dispatch(notifyApp(createErrorNotification('Failed to load services from Jaeger', error))); } } onLoadOptions = async (selectedOptions: CascaderOption[]) => { const service = selectedOptions[0].value; if (selectedOptions.length === 1) { // Load operations const operations: string[] = await this.findOperations(service); if (!this._isMounted) { return; } const allOperationsOption: CascaderOption = { label: '[ALL]', value: ALL_OPERATIONS_KEY, }; const operationOptions: CascaderOption[] = [ allOperationsOption, ...operations.sort().map((operation) => ({ label: operation, value: operation, isLeaf: false, })), ]; this.setState((state) => { const serviceOptions = state.serviceOptions.map((serviceOption) => { if (serviceOption.value === service) { return { ...serviceOption, children: operationOptions, }; } return serviceOption; }); return { serviceOptions }; }); } else if (selectedOptions.length === 2) { // Load traces const operationValue = selectedOptions[1].value; const operation = operationValue === ALL_OPERATIONS_KEY ? '' : operationValue; const traces: any[] = await this.findTraces(service, operation); if (!this._isMounted) { return; } let traceOptions: CascaderOption[] = traces.map((trace) => ({ label: getLabelFromTrace(trace), value: trace.traceID, })); if (traceOptions.length === 0) { traceOptions = [ { label: '[No traces in time range]', value: NO_TRACES_KEY, }, ]; } this.setState((state) => { // Place new traces into the correct service/operation sub-tree const serviceOptions = state.serviceOptions.map((serviceOption) => { if (serviceOption.value === service && serviceOption.children) { const operationOptions = serviceOption.children.map((operationOption) => { if (operationOption.value === operationValue) { return { ...operationOption, children: traceOptions, }; } return operationOption; }); return { ...serviceOption, children: operationOptions, }; } return serviceOption; }); return { serviceOptions }; }); } }; findOperations = async (service: string) => { const { datasource } = this.props; const url = `/api/services/${encodeURIComponent(service)}/operations`; try { return await datasource.metadataRequest(url); } catch (error) { dispatch(notifyApp(createErrorNotification('Failed to load operations from Jaeger', error))); } return []; }; findTraces = async (service: string, operation?: string) => { const { datasource } = this.props; const { start, end } = datasource.getTimeRange(); const traceSearch = { start, end, service, operation, limit: 10, lookback: '1h', maxDuration: '', minDuration: '', }; const url = '/api/traces'; try { return await datasource.metadataRequest(url, traceSearch); } catch (error) { dispatch(notifyApp(createErrorNotification('Failed to load traces from Jaeger', error))); } return []; }; onSelectTrace = (values: string[], selectedOptions: CascaderOption[]) => { const { query, onChange, onRunQuery } = this.props; if (selectedOptions.length === 3) { const traceID = selectedOptions[2].value; onChange({ ...query, query: traceID }); onRunQuery(); } }; render() { const { query, onChange } = this.props; const { serviceOptions } = this.state; const cascaderOptions = serviceOptions && serviceOptions.length ? serviceOptions : noTracesFoundOptions; return ( <>
Traces
onChange({ ...query, query: e.currentTarget.value, }) } />
); } } const noTracesFoundOptions = [ { label: 'No traces found', value: 'no_traces', isLeaf: true, // Cannot be disabled because then cascader shows 'loading' for some reason. // disabled: true, }, ]; export default JaegerQueryField;