Explore: Add opt-in config for Node Graph in Jaeger, Zipkin, and Tempo (#39958)

This commit is contained in:
Connor Lindsey 2021-10-06 13:39:14 -06:00 committed by GitHub
parent 4a91ceeb1e
commit 3a8d04603f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 173 additions and 27 deletions

View File

@ -39,6 +39,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings')
### Node Graph
This is a configuration for the beta Node Graph visualization. The Node Graph is shown after the trace view is loaded and is disabled by default.
-- **Enable Node Graph -** Enables the Node Graph visualization.
## Query traces
You can query and display traces from Jaeger via [Explore]({{< relref "../explore/_index.md" >}}).

View File

@ -38,6 +38,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings')
### Node Graph
This is a configuration for the beta Node Graph visualization. The Node Graph is shown after the trace view is loaded and is disabled by default.
-- **Enable Node Graph -** Enables the Node Graph visualization.
## Query traces
You can query and display traces from Tempo via [Explore]({{< relref "../explore/_index.md" >}}).

View File

@ -39,6 +39,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings')
### Node Graph
This is a configuration for the beta Node Graph visualization. The Node Graph is shown after the trace view is loaded and is disabled by default.
-- **Enable Node Graph -** Enables the Node Graph visualization.
## Query traces
Querying and displaying traces from Zipkin is available via [Explore]({{< relref "../explore" >}}).

View File

@ -0,0 +1,57 @@
import React from 'react';
import { css } from '@emotion/css';
import {
DataSourceJsonData,
DataSourcePluginOptionsEditorProps,
GrafanaTheme,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, useStyles } from '@grafana/ui';
export interface NodeGraphOptions {
enabled?: boolean;
}
export interface NodeGraphData extends DataSourceJsonData {
nodeGraph?: NodeGraphOptions;
}
interface Props extends DataSourcePluginOptionsEditorProps<NodeGraphData> {}
export function NodeGraphSettings({ options, onOptionsChange }: Props) {
const styles = useStyles(getStyles);
return (
<div className={styles.container}>
<h3 className="page-heading">Node Graph</h3>
<InlineFieldRow className={styles.row}>
<InlineField
tooltip="Enables the Node Graph visualization in the trace viewer."
label="Enable Node Graph"
labelWidth={26}
>
<InlineSwitch
value={options.jsonData.nodeGraph?.enabled}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'nodeGraph', {
...options.jsonData.nodeGraph,
enabled: event.currentTarget.checked,
})
}
/>
</InlineField>
</InlineFieldRow>
</div>
);
}
const getStyles = (theme: GrafanaTheme) => ({
container: css`
label: container;
width: 100%;
`,
row: css`
label: row;
align-items: baseline;
`,
});

View File

@ -1,5 +1,6 @@
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
import React from 'react';
@ -15,7 +16,12 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
onChange={onOptionsChange}
/>
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
<div className="gf-form-group">
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>
</>
);
};

View File

@ -4,7 +4,7 @@ import { DataQueryRequest, DataSourceInstanceSettings, dateTime, FieldType, Plug
import { backendSrv } from 'app/core/services/backend_srv';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
import { JaegerDatasource } from './datasource';
import { JaegerDatasource, JaegerJsonData } from './datasource';
import mockJson from './mockJsonResponse.json';
import {
testResponse,
@ -222,7 +222,7 @@ function setupFetchMock(response: any, mock?: any) {
return fetchMock;
}
const defaultSettings: DataSourceInstanceSettings = {
const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
id: 0,
uid: '0',
type: 'tracing',
@ -237,7 +237,11 @@ const defaultSettings: DataSourceInstanceSettings = {
module: '',
baseUrl: '',
},
jsonData: {},
jsonData: {
nodeGraph: {
enabled: true,
},
},
};
const defaultQuery: DataQueryRequest<JaegerQuery> = {

View File

@ -6,6 +6,7 @@ import {
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
dateMath,
DateTime,
FieldType,
@ -20,11 +21,21 @@ import { createGraphFrames } from './graphTransform';
import { JaegerQuery } from './types';
import { convertTagsLogfmt } from './util';
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
export interface JaegerJsonData extends DataSourceJsonData {
nodeGraph?: NodeGraphOptions;
}
export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData> {
uploadedJson: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings, private readonly timeSrv: TimeSrv = getTimeSrv()) {
nodeGraph?: NodeGraphOptions;
constructor(
private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
private readonly timeSrv: TimeSrv = getTimeSrv()
) {
super(instanceSettings);
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
}
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> {
@ -47,8 +58,12 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
if (!traceData) {
return { data: [emptyTraceDataFrame] };
}
let data = [createTraceFrame(traceData)];
if (this.nodeGraph?.enabled) {
data.push(...createGraphFrames(traceData));
}
return {
data: [createTraceFrame(traceData), ...createGraphFrames(traceData)],
data,
};
})
);
@ -61,7 +76,11 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery> {
try {
const traceData = JSON.parse(this.uploadedJson as string).data[0];
return of({ data: [createTraceFrame(traceData), ...createGraphFrames(traceData)] });
let data = [createTraceFrame(traceData)];
if (this.nodeGraph?.enabled) {
data.push(...createGraphFrames(traceData));
}
return of({ data });
} catch (error) {
return of({ error: { message: 'JSON is not valid Jaeger format' }, data: [] });
}

View File

@ -5,6 +5,7 @@ import React from 'react';
import { ServiceMapSettings } from './ServiceMapSettings';
import { config } from '@grafana/runtime';
import { SearchSettings } from './SearchSettings';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@ -31,6 +32,9 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<SearchSettings options={options} onOptionsChange={onOptionsChange} />
</div>
)}
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>
</>
);
};

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { DataSourcePluginOptionsEditorProps, GrafanaTheme, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, useStyles } from '@grafana/ui';
import React from 'react';
import { TempoJsonData } from './datasource';
import { TempoJsonData } from '../datasource';
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}

View File

@ -3,7 +3,7 @@ import { DataSourcePluginOptionsEditorProps, GrafanaTheme, updateDatasourcePlugi
import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, useStyles } from '@grafana/ui';
import React from 'react';
import { TempoJsonData } from './datasource';
import { TempoJsonData } from '../datasource';
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}

View File

@ -12,7 +12,7 @@ import {
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { BackendDataSourceResponse, FetchResponse, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
import { DEFAULT_LIMIT, TempoDatasource, TempoQuery } from './datasource';
import { DEFAULT_LIMIT, TempoJsonData, TempoDatasource, TempoQuery } from './datasource';
import mockJson from './mockJsonResponse.json';
describe('Tempo data source', () => {
@ -233,7 +233,7 @@ function setupBackendSrv(frame: DataFrame) {
} as any);
}
const defaultSettings: DataSourceInstanceSettings = {
const defaultSettings: DataSourceInstanceSettings<TempoJsonData> = {
id: 0,
uid: '0',
type: 'tracing',
@ -247,7 +247,11 @@ const defaultSettings: DataSourceInstanceSettings = {
module: '',
baseUrl: '',
},
jsonData: {},
jsonData: {
nodeGraph: {
enabled: true,
},
},
};
const totalsPromMetric = new MutableDataFrame({

View File

@ -27,6 +27,7 @@ import {
createTableFrameFromSearch,
} from './resultTransformer';
import { tokenizer } from './syntax';
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
// search = Loki search, nativeSearch = Tempo search for backwards compatibility
export type TempoQueryType = 'search' | 'traceId' | 'serviceMap' | 'upload' | 'nativeSearch';
@ -39,6 +40,7 @@ export interface TempoJsonData extends DataSourceJsonData {
search?: {
hide?: boolean;
};
nodeGraph?: NodeGraphOptions;
}
export type TempoQuery = {
@ -64,6 +66,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
search?: {
hide?: boolean;
};
nodeGraph?: NodeGraphOptions;
uploadedJson?: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings<TempoJsonData>) {
@ -71,6 +74,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
this.tracesToLogs = instanceSettings.jsonData.tracesToLogs;
this.serviceMap = instanceSettings.jsonData.serviceMap;
this.search = instanceSettings.jsonData.search;
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
}
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> {
@ -137,7 +141,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
if (!otelTraceData.batches) {
subQueries.push(of({ error: { message: 'JSON is not valid OpenTelemetry format' }, data: [] }));
} else {
subQueries.push(of(transformFromOTEL(otelTraceData.batches)));
subQueries.push(of(transformFromOTEL(otelTraceData.batches, this.nodeGraph?.enabled)));
}
} else {
subQueries.push(of({ data: [], state: LoadingState.Done }));
@ -156,7 +160,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
if (response.error) {
return response;
}
return transformTrace(response);
return transformTrace(response, this.nodeGraph?.enabled);
})
)
);

View File

@ -1,6 +1,6 @@
import { DataSourcePlugin } from '@grafana/data';
import CheatSheet from './CheatSheet';
import { ConfigEditor } from './ConfigEditor';
import { ConfigEditor } from './configuration/ConfigEditor';
import { TempoDatasource } from './datasource';
import { TempoQueryField } from './QueryField';

View File

@ -237,7 +237,8 @@ function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
}
export function transformFromOTLP(
traceData: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[]
traceData: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
nodeGraph = false
): DataQueryResponse {
const frame = new MutableDataFrame({
fields: [
@ -283,7 +284,12 @@ export function transformFromOTLP(
return { error: { message: 'JSON is not valid OpenTelemetry format' }, data: [] };
}
return { data: [frame, ...createGraphFrames(frame)] };
let data = [frame];
if (nodeGraph) {
data.push(...(createGraphFrames(frame) as MutableDataFrame[]));
}
return { data };
}
/**
@ -463,7 +469,7 @@ function getOTLPEvents(logs: TraceLog[]): collectorTypes.opentelemetryProto.trac
return events;
}
export function transformTrace(response: DataQueryResponse): DataQueryResponse {
export function transformTrace(response: DataQueryResponse, nodeGraph = false): DataQueryResponse {
// We need to parse some of the fields which contain stringified json.
// Seems like we can't just map the values as the frame we got from backend has some default processing
// and will stringify the json back when we try to set it. So we create a new field and swap it instead.
@ -475,9 +481,14 @@ export function transformTrace(response: DataQueryResponse): DataQueryResponse {
parseJsonFields(frame);
let data = [...response.data];
if (nodeGraph) {
data.push(...createGraphFrames(frame));
}
return {
...response,
data: [...response.data, ...createGraphFrames(frame)],
data,
};
}

View File

@ -1,5 +1,6 @@
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings';
import React from 'react';
@ -15,7 +16,13 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
onChange={onOptionsChange}
/>
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
<div className="gf-form-group">
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>
</>
);
};

View File

@ -6,6 +6,7 @@ import {
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
FieldType,
MutableDataFrame,
} from '@grafana/data';
@ -15,11 +16,18 @@ import { apiPrefix } from './constants';
import { ZipkinQuery, ZipkinSpan } from './types';
import { createGraphFrames } from './utils/graphTransform';
import { transformResponse } from './utils/transforms';
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
export interface ZipkinJsonData extends DataSourceJsonData {
nodeGraph?: NodeGraphOptions;
}
export class ZipkinDatasource extends DataSourceApi<ZipkinQuery, ZipkinJsonData> {
uploadedJson: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings) {
nodeGraph?: NodeGraphOptions;
constructor(private instanceSettings: DataSourceInstanceSettings<ZipkinJsonData>) {
super(instanceSettings);
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
}
query(options: DataQueryRequest<ZipkinQuery>): Observable<DataQueryResponse> {
@ -31,7 +39,7 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
try {
const traceData = JSON.parse(this.uploadedJson as string);
return of(responseToDataQueryResponse({ data: traceData }));
return of(responseToDataQueryResponse({ data: traceData }, this.nodeGraph?.enabled));
} catch (error) {
return of({ error: { message: 'JSON is not valid Zipkin format' }, data: [] });
}
@ -39,7 +47,7 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
if (target.query) {
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe(
map(responseToDataQueryResponse)
map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled))
);
}
return of(emptyDataQueryResponse);
@ -75,9 +83,13 @@ export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
}
}
function responseToDataQueryResponse(response: { data: ZipkinSpan[] }): DataQueryResponse {
function responseToDataQueryResponse(response: { data: ZipkinSpan[] }, nodeGraph = false): DataQueryResponse {
let data = response?.data ? [transformResponse(response?.data)] : [];
if (nodeGraph) {
data.push(...createGraphFrames(response?.data));
}
return {
data: response?.data ? [transformResponse(response?.data), ...createGraphFrames(response?.data)] : [],
data,
};
}