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:
Máté Szabó 2024-01-15 13:02:00 +01:00 committed by GitHub
parent 6a36525d61
commit e2b706fdd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 258 additions and 1 deletions

View File

@ -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.

View File

@ -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) =>

View File

@ -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: [] });
}

View File

@ -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,
},
],
});
});
});

View 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,
});
}
}

View File

@ -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;
};