mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Add integration with Jeager Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split. Modifies build so that this branch docker images are pushed to docker hub Add a traceui dir with docker-compose and provision files for demoing.:wq * Enable docker logger plugin to send logs to loki * Add placeholder zipkin datasource * Fixed rebase issues, added enhanceDataFrame to non-legacy code path * Trace selector for jaeger query field * Fix logs default mode for Loki * Fix loading jaeger query field services on split * Updated grafana image in traceui/compose file * Fix prettier error * Hide behind feature flag, clean up unused code. * Fix tests * Fix tests * Cleanup code and review feedback * Remove traceui directory * Remove circle build changes * Fix feature toggles object * Fix merge issues * Fix some null errors * Fix test after strict null changes * Review feedback fixes * Fix toggle name Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
203 lines
5.8 KiB
TypeScript
203 lines
5.8 KiB
TypeScript
import React from 'react';
|
|
import { JaegerDatasource, JaegerQuery } from './datasource';
|
|
import { ButtonCascader, CascaderOption } from '@grafana/ui';
|
|
|
|
import { ExploreQueryFieldProps } from '@grafana/data';
|
|
|
|
const ALL_OPERATIONS_KEY = '__ALL__';
|
|
const NO_TRACES_KEY = '__NO_TRACES__';
|
|
|
|
type Props = ExploreQueryFieldProps<JaegerDatasource, JaegerQuery>;
|
|
interface State {
|
|
serviceOptions: CascaderOption[];
|
|
}
|
|
|
|
function getLabelFromTrace(trace: any): string {
|
|
const firstSpan = trace.spans && trace.spans[0];
|
|
if (firstSpan) {
|
|
return `${firstSpan.operationName} [${firstSpan.duration} ms]`;
|
|
}
|
|
return trace.traceID;
|
|
}
|
|
|
|
export class JaegerQueryField extends React.PureComponent<Props, State> {
|
|
constructor(props: Props, context: React.Context<any>) {
|
|
super(props, context);
|
|
this.state = {
|
|
serviceOptions: [],
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.getServices();
|
|
}
|
|
|
|
async getServices() {
|
|
const url = '/api/services';
|
|
const { datasource } = this.props;
|
|
try {
|
|
const res = await datasource.metadataRequest(url);
|
|
if (res) {
|
|
const services = res as string[];
|
|
const serviceOptions: CascaderOption[] = services.sort().map(service => ({
|
|
label: service,
|
|
value: service,
|
|
isLeaf: false,
|
|
}));
|
|
this.setState({ serviceOptions });
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
onLoadOptions = async (selectedOptions: CascaderOption[]) => {
|
|
const service = selectedOptions[0].value;
|
|
if (selectedOptions.length === 1) {
|
|
// Load operations
|
|
const operations: string[] = await this.findOperations(service);
|
|
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);
|
|
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) {
|
|
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/${service}/operations`;
|
|
try {
|
|
return await datasource.metadataRequest(url);
|
|
} catch (error) {
|
|
console.error(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) {
|
|
console.error(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;
|
|
|
|
return (
|
|
<>
|
|
<div className="gf-form-inline gf-form-inline--nowrap">
|
|
<div className="gf-form flex-shrink-0">
|
|
<ButtonCascader options={serviceOptions} onChange={this.onSelectTrace} loadData={this.onLoadOptions}>
|
|
Traces
|
|
</ButtonCascader>
|
|
</div>
|
|
<div className="gf-form gf-form--grow flex-shrink-1">
|
|
<div className={'slate-query-field__wrapper'}>
|
|
<div className="slate-query-field">
|
|
<input
|
|
style={{ width: '100%' }}
|
|
value={query.query || ''}
|
|
onChange={e =>
|
|
onChange({
|
|
...query,
|
|
query: e.currentTarget.value,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default JaegerQueryField;
|