mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Jaeger: Add service dependency graph support (#72200)
* Jaeger: Add service dependency graph support Add support for visualizing Jaeger's service dependency graph via the Jaeger data source. Per the discussion[1], this is done by proxying the internal Jaeger HTTP API endpoint used by Jaeger's own UI for fetching graph data, and transforming it into a format suitable for the node graph panel in Grafana. --- [1] https://github.com/grafana/grafana/discussions/52035 * Small lint fixes * Type fix --------- Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
This commit is contained in:
parent
6a36525d61
commit
e2b706fdd3
@ -369,3 +369,8 @@ To configure this feature, see the [introduction to exemplars][exemplars] docume
|
|||||||
[variable-syntax]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/variables/variable-syntax"
|
[variable-syntax]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/variables/variable-syntax"
|
||||||
[variable-syntax]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/variables/variable-syntax"
|
[variable-syntax]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/variables/variable-syntax"
|
||||||
{{% /docs/reference %}}
|
{{% /docs/reference %}}
|
||||||
|
|
||||||
|
## Visualizing the dependency graph
|
||||||
|
|
||||||
|
If service dependency information is available in Jaeger, it can be visualized in Grafana.
|
||||||
|
Use the Jaeger data source with the "Dependency Graph" query type on a Node Graph panel for this.
|
||||||
|
@ -36,6 +36,8 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props)
|
|||||||
switch (query.queryType) {
|
switch (query.queryType) {
|
||||||
case 'search':
|
case 'search':
|
||||||
return <SearchForm datasource={datasource} query={query} onChange={onChange} />;
|
return <SearchForm datasource={datasource} query={query} onChange={onChange} />;
|
||||||
|
case 'dependencyGraph':
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
@ -79,6 +81,7 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props)
|
|||||||
options={[
|
options={[
|
||||||
{ value: 'search', label: 'Search' },
|
{ value: 'search', label: 'Search' },
|
||||||
{ value: undefined, label: 'TraceID' },
|
{ value: undefined, label: 'TraceID' },
|
||||||
|
{ value: 'dependencyGraph', label: 'Dependency graph' },
|
||||||
]}
|
]}
|
||||||
value={query.queryType}
|
value={query.queryType}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
|
@ -22,6 +22,7 @@ import { SpanBarOptions } from 'app/features/explore/TraceView/components';
|
|||||||
|
|
||||||
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
||||||
import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams';
|
import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams';
|
||||||
|
import { mapJaegerDependenciesResponse } from './dependencyGraphTransform';
|
||||||
import { createGraphFrames } from './graphTransform';
|
import { createGraphFrames } from './graphTransform';
|
||||||
import { createTableFrame, createTraceFrame } from './responseTransform';
|
import { createTableFrame, createTraceFrame } from './responseTransform';
|
||||||
import { JaegerQuery } from './types';
|
import { JaegerQuery } from './types';
|
||||||
@ -65,6 +66,14 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
|||||||
return of({ data: [emptyTraceDataFrame] });
|
return of({ data: [emptyTraceDataFrame] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the internal Jaeger /dependencies API for rendering the dependency graph.
|
||||||
|
if (target.queryType === 'dependencyGraph') {
|
||||||
|
const timeRange = this.timeSrv.timeRange();
|
||||||
|
const endTs = getTime(timeRange.to, true) / 1000;
|
||||||
|
const lookback = endTs - getTime(timeRange.from, false) / 1000;
|
||||||
|
return this._request('/api/dependencies', { endTs, lookback }).pipe(map(mapJaegerDependenciesResponse));
|
||||||
|
}
|
||||||
|
|
||||||
if (target.queryType === 'search' && !this.isSearchFormValid(target)) {
|
if (target.queryType === 'search' && !this.isSearchFormValid(target)) {
|
||||||
return of({ error: { message: 'You must select a service.' }, data: [] });
|
return of({ error: { message: 'You must select a service.' }, data: [] });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
import { mapJaegerDependenciesResponse } from './dependencyGraphTransform';
|
||||||
|
|
||||||
|
describe('dependencyGraphTransform', () => {
|
||||||
|
it('should transform Jaeger dependencies API response', () => {
|
||||||
|
const data = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
parent: 'serviceA',
|
||||||
|
child: 'serviceB',
|
||||||
|
callCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: 'serviceA',
|
||||||
|
child: 'serviceC',
|
||||||
|
callCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: 'serviceB',
|
||||||
|
child: 'serviceC',
|
||||||
|
callCount: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 0,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = mapJaegerDependenciesResponse({ data });
|
||||||
|
expect(res).toMatchObject({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
values: ['serviceA', 'serviceB', 'serviceC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
values: ['serviceA', 'serviceB', 'serviceC'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: { preferredVisualisationType: 'nodeGraph' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
values: ['serviceA--serviceB', 'serviceA--serviceC', 'serviceB--serviceC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'target',
|
||||||
|
type: 'string',
|
||||||
|
values: ['serviceB', 'serviceC', 'serviceC'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'source',
|
||||||
|
type: 'string',
|
||||||
|
values: ['serviceA', 'serviceA', 'serviceB'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { displayName: 'Call count' },
|
||||||
|
name: 'mainstat',
|
||||||
|
type: 'string',
|
||||||
|
values: [1, 2, 3],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: { preferredVisualisationType: 'nodeGraph' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform Jaeger API error', () => {
|
||||||
|
const data = {
|
||||||
|
total: 0,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: 400,
|
||||||
|
msg: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = mapJaegerDependenciesResponse({ data });
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
data: [],
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax',
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
125
public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts
Normal file
125
public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataQueryResponse,
|
||||||
|
FieldType,
|
||||||
|
MutableDataFrame,
|
||||||
|
NodeGraphDataFrameFieldNames as Fields,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
import { JaegerServiceDependency } from './types';
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
[Fields.id]: string;
|
||||||
|
[Fields.title]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Edge {
|
||||||
|
[Fields.id]: string;
|
||||||
|
[Fields.target]: string;
|
||||||
|
[Fields.source]: string;
|
||||||
|
[Fields.mainStat]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error schema used by the Jaeger dependencies API.
|
||||||
|
*/
|
||||||
|
interface JaegerDependenciesResponseError {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JaegerDependenciesResponse {
|
||||||
|
data?: {
|
||||||
|
errors?: JaegerDependenciesResponseError[];
|
||||||
|
data?: JaegerServiceDependency[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a Jaeger dependencies API response to a Grafana {@link DataQueryResponse}.
|
||||||
|
* @param response Raw response data from the API proxy.
|
||||||
|
*/
|
||||||
|
export function mapJaegerDependenciesResponse(response: JaegerDependenciesResponse): DataQueryResponse {
|
||||||
|
const errors = response?.data?.errors;
|
||||||
|
if (errors) {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
errors: errors.map((e: JaegerDependenciesResponseError) => ({ message: e.msg, status: e.code })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const dependencies = response?.data?.data;
|
||||||
|
if (dependencies) {
|
||||||
|
return {
|
||||||
|
data: convertDependenciesToGraph(dependencies),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of Jaeger service dependencies to a Grafana {@link DataFrame} array suitable for the node graph panel.
|
||||||
|
* @param dependencies List of Jaeger service dependencies as returned by the Jaeger dependencies API.
|
||||||
|
*/
|
||||||
|
function convertDependenciesToGraph(dependencies: JaegerServiceDependency[]): DataFrame[] {
|
||||||
|
const servicesByName = new Map<string, Node>();
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
|
||||||
|
for (const dependency of dependencies) {
|
||||||
|
addServiceNode(dependency.parent, servicesByName);
|
||||||
|
addServiceNode(dependency.child, servicesByName);
|
||||||
|
|
||||||
|
edges.push({
|
||||||
|
[Fields.id]: dependency.parent + '--' + dependency.child,
|
||||||
|
[Fields.target]: dependency.child,
|
||||||
|
[Fields.source]: dependency.parent,
|
||||||
|
[Fields.mainStat]: dependency.callCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesFrame = new MutableDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: Fields.id, type: FieldType.string },
|
||||||
|
{ name: Fields.title, type: FieldType.string },
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
preferredVisualisationType: 'nodeGraph',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgesFrame = new MutableDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: Fields.id, type: FieldType.string },
|
||||||
|
{ name: Fields.target, type: FieldType.string },
|
||||||
|
{ name: Fields.source, type: FieldType.string },
|
||||||
|
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Call count' } },
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
preferredVisualisationType: 'nodeGraph',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const node of servicesByName.values()) {
|
||||||
|
nodesFrame.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
edgesFrame.add(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [nodesFrame, edgesFrame];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to register a service node in the dependency graph.
|
||||||
|
* @param service Name of the service to register.
|
||||||
|
* @param servicesByName Map of service nodes keyed name.
|
||||||
|
*/
|
||||||
|
function addServiceNode(service: string, servicesByName: Map<string, Node>) {
|
||||||
|
if (!servicesByName.has(service)) {
|
||||||
|
servicesByName.set(service, {
|
||||||
|
[Fields.id]: service,
|
||||||
|
[Fields.title]: service,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,7 @@ export type JaegerQuery = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
} & DataQuery;
|
} & DataQuery;
|
||||||
|
|
||||||
export type JaegerQueryType = 'search' | 'upload';
|
export type JaegerQueryType = 'search' | 'upload' | 'dependencyGraph';
|
||||||
|
|
||||||
export type JaegerResponse = {
|
export type JaegerResponse = {
|
||||||
data: TraceResponse[];
|
data: TraceResponse[];
|
||||||
@ -72,3 +72,12 @@ export type JaegerResponse = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
errors?: string[] | null;
|
errors?: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition for service dependencies as returned by the Jaeger dependencies API.
|
||||||
|
*/
|
||||||
|
export type JaegerServiceDependency = {
|
||||||
|
parent: string;
|
||||||
|
child: string;
|
||||||
|
callCount: number;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user