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-cloud/ -> /docs/grafana/<GRAFANA VERSION>/dashboards/variables/variable-syntax"
|
||||
{{% /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) {
|
||||
case 'search':
|
||||
return <SearchForm datasource={datasource} query={query} onChange={onChange} />;
|
||||
case 'dependencyGraph':
|
||||
return null;
|
||||
default:
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
@ -79,6 +81,7 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props)
|
||||
options={[
|
||||
{ value: 'search', label: 'Search' },
|
||||
{ value: undefined, label: 'TraceID' },
|
||||
{ value: 'dependencyGraph', label: 'Dependency graph' },
|
||||
]}
|
||||
value={query.queryType}
|
||||
onChange={(v) =>
|
||||
|
@ -22,6 +22,7 @@ import { SpanBarOptions } from 'app/features/explore/TraceView/components';
|
||||
|
||||
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
||||
import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams';
|
||||
import { mapJaegerDependenciesResponse } from './dependencyGraphTransform';
|
||||
import { createGraphFrames } from './graphTransform';
|
||||
import { createTableFrame, createTraceFrame } from './responseTransform';
|
||||
import { JaegerQuery } from './types';
|
||||
@ -65,6 +66,14 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
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)) {
|
||||
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;
|
||||
} & DataQuery;
|
||||
|
||||
export type JaegerQueryType = 'search' | 'upload';
|
||||
export type JaegerQueryType = 'search' | 'upload' | 'dependencyGraph';
|
||||
|
||||
export type JaegerResponse = {
|
||||
data: TraceResponse[];
|
||||
@ -72,3 +72,12 @@ export type JaegerResponse = {
|
||||
offset: number;
|
||||
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