From e2b706fdd3c15b52b187401394249eba50fe6f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Szab=C3=B3?= Date: Mon, 15 Jan 2024 13:02:00 +0100 Subject: [PATCH] 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 --- docs/sources/datasources/jaeger/_index.md | 5 + .../jaeger/components/QueryEditor.tsx | 3 + .../plugins/datasource/jaeger/datasource.ts | 9 ++ .../jaeger/dependencyGraphTransform.test.ts | 106 +++++++++++++++ .../jaeger/dependencyGraphTransform.ts | 125 ++++++++++++++++++ public/app/plugins/datasource/jaeger/types.ts | 11 +- 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/jaeger/dependencyGraphTransform.test.ts create mode 100644 public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 91143aed8b5..4f0e9ee0fa7 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -369,3 +369,8 @@ To configure this feature, see the [introduction to exemplars][exemplars] docume [variable-syntax]: "/docs/grafana/ -> /docs/grafana//dashboards/variables/variable-syntax" [variable-syntax]: "/docs/grafana-cloud/ -> /docs/grafana//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. diff --git a/public/app/plugins/datasource/jaeger/components/QueryEditor.tsx b/public/app/plugins/datasource/jaeger/components/QueryEditor.tsx index 6b0a7c3a9a4..ed9c9e0f084 100644 --- a/public/app/plugins/datasource/jaeger/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/jaeger/components/QueryEditor.tsx @@ -36,6 +36,8 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props) switch (query.queryType) { case 'search': return ; + case 'dependencyGraph': + return null; default: return ( @@ -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) => diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index ab5c89e7407..2ccee1ad1f1 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -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 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: [] }); } diff --git a/public/app/plugins/datasource/jaeger/dependencyGraphTransform.test.ts b/public/app/plugins/datasource/jaeger/dependencyGraphTransform.test.ts new file mode 100644 index 00000000000..e17621ba45c --- /dev/null +++ b/public/app/plugins/datasource/jaeger/dependencyGraphTransform.test.ts @@ -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, + }, + ], + }); + }); +}); diff --git a/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts b/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts new file mode 100644 index 00000000000..0e8fe6dce46 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/dependencyGraphTransform.ts @@ -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(); + 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) { + if (!servicesByName.has(service)) { + servicesByName.set(service, { + [Fields.id]: service, + [Fields.title]: service, + }); + } +} diff --git a/public/app/plugins/datasource/jaeger/types.ts b/public/app/plugins/datasource/jaeger/types.ts index 45ee89fbeb0..fe6778957f6 100644 --- a/public/app/plugins/datasource/jaeger/types.ts +++ b/public/app/plugins/datasource/jaeger/types.ts @@ -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; +};