mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add opt-in config for Node Graph in Jaeger, Zipkin, and Tempo (#39958)
This commit is contained in:
parent
4a91ceeb1e
commit
3a8d04603f
@ -39,6 +39,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||

|
||||
|
||||
### 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" >}}).
|
||||
|
@ -38,6 +38,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||

|
||||
|
||||
### 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" >}}).
|
||||
|
@ -39,6 +39,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
|
||||
|
||||

|
||||
|
||||
### 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" >}}).
|
||||
|
57
public/app/core/components/NodeGraphSettings.tsx
Normal file
57
public/app/core/components/NodeGraphSettings.tsx
Normal 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;
|
||||
`,
|
||||
});
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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> = {
|
||||
|
@ -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: [] });
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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> {}
|
||||
|
@ -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> {}
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user