Tempo: Add error metric to service graph (#40774)

* Add typings

* Query error metric for service graph
pull/40820/head
Andrej Ocenas 4 years ago committed by GitHub
parent ca4b35e596
commit 73e66d97a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      public/app/plugins/datasource/tempo/datasource.test.ts
  2. 3
      public/app/plugins/datasource/tempo/datasource.ts
  3. 26
      public/app/plugins/datasource/tempo/graphTransform.test.ts
  4. 96
      public/app/plugins/datasource/tempo/graphTransform.ts

@ -209,7 +209,7 @@ const backendSrvWithPrometheus = {
if (uid === 'prom') { if (uid === 'prom') {
return { return {
query() { query() {
return of({ data: [totalsPromMetric, secondsPromMetric] }); return of({ data: [totalsPromMetric, secondsPromMetric, failedPromMetric] });
}, },
}; };
} }
@ -280,6 +280,19 @@ const secondsPromMetric = new MutableDataFrame({
], ],
}); });
const failedPromMetric = new MutableDataFrame({
refId: 'traces_service_graph_request_failed_total',
fields: [
{ name: 'Time', values: [1628169788000, 1628169788000] },
{ name: 'client', values: ['app', 'lb'] },
{ name: 'instance', values: ['127.0.0.1:12345', '127.0.0.1:12345'] },
{ name: 'job', values: ['local_scrape', 'local_scrape'] },
{ name: 'server', values: ['db', 'app'] },
{ name: 'tempo_config', values: ['default', 'default'] },
{ name: 'Value #traces_service_graph_request_failed_total', values: [2, 15] },
],
});
const mockInvalidJson = { const mockInvalidJson = {
batches: [ batches: [
{ {

@ -292,9 +292,6 @@ function serviceMapQuery(request: DataQueryRequest<TempoQuery>, datasourceUid: s
data: mapPromMetricsToServiceMap(responses, request.range), data: mapPromMetricsToServiceMap(responses, request.range),
state: LoadingState.Done, state: LoadingState.Done,
}; };
}),
catchError((error) => {
return of({ error: { message: error.message }, data: [] });
}) })
); );
} }

@ -64,16 +64,21 @@ describe('mapPromMetricsToServiceMap', () => {
from: dateTime('2000-01-01T00:00:00'), from: dateTime('2000-01-01T00:00:00'),
to: dateTime('2000-01-01T00:01:00'), to: dateTime('2000-01-01T00:01:00'),
}; };
const [nodes, edges] = mapPromMetricsToServiceMap([{ data: [totalsPromMetric, secondsPromMetric] }], { const [nodes, edges] = mapPromMetricsToServiceMap(
...range, [{ data: [totalsPromMetric, secondsPromMetric, failedPromMetric] }],
raw: range, {
}); ...range,
raw: range,
}
);
expect(nodes.fields).toMatchObject([ expect(nodes.fields).toMatchObject([
{ name: 'id', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'id', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'title', values: new ArrayVector(['db', 'app', 'lb']) }, { name: 'title', values: new ArrayVector(['db', 'app', 'lb']) },
{ name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) }, { name: 'mainStat', values: new ArrayVector([1000, 2000, NaN]) },
{ name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) }, { name: 'secondaryStat', values: new ArrayVector([0.17, 0.33, NaN]) },
{ name: 'arc__success', values: new ArrayVector([0.8, 0.25, 1]) },
{ name: 'arc__failed', values: new ArrayVector([0.2, 0.75, 0]) },
]); ]);
expect(edges.fields).toMatchObject([ expect(edges.fields).toMatchObject([
{ name: 'id', values: new ArrayVector(['app_db', 'lb_app']) }, { name: 'id', values: new ArrayVector(['app_db', 'lb_app']) },
@ -134,3 +139,16 @@ const secondsPromMetric = new MutableDataFrame({
{ name: 'Value #traces_service_graph_request_server_seconds_sum', values: [10, 40] }, { name: 'Value #traces_service_graph_request_server_seconds_sum', values: [10, 40] },
], ],
}); });
const failedPromMetric = new MutableDataFrame({
refId: 'traces_service_graph_request_failed_total',
fields: [
{ name: 'Time', values: [1628169788000, 1628169788000] },
{ name: 'client', values: ['app', 'lb'] },
{ name: 'instance', values: ['127.0.0.1:12345', '127.0.0.1:12345'] },
{ name: 'job', values: ['local_scrape', 'local_scrape'] },
{ name: 'server', values: ['db', 'app'] },
{ name: 'tempo_config', values: ['default', 'default'] },
{ name: 'Value #traces_service_graph_request_failed_total', values: [2, 15] },
],
});

@ -1,8 +1,8 @@
import { groupBy } from 'lodash';
import { import {
DataFrame, DataFrame,
DataFrameView, DataFrameView,
DataQueryResponse, DataQueryResponse,
FieldColorModeId,
FieldDTO, FieldDTO,
MutableDataFrame, MutableDataFrame,
NodeGraphDataFrameFieldNames as Fields, NodeGraphDataFrameFieldNames as Fields,
@ -10,6 +10,9 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../core/utils/tracing'; import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../core/utils/tracing';
/**
* Row in a trace dataFrame
*/
interface Row { interface Row {
traceID: string; traceID: string;
spanID: string; spanID: string;
@ -129,10 +132,12 @@ function findTraceDuration(view: DataFrameView<Row>): number {
const secondsMetric = 'traces_service_graph_request_server_seconds_sum'; const secondsMetric = 'traces_service_graph_request_server_seconds_sum';
const totalsMetric = 'traces_service_graph_request_total'; const totalsMetric = 'traces_service_graph_request_total';
const failedMetric = 'traces_service_graph_request_failed_total';
export const serviceMapMetrics = [ export const serviceMapMetrics = [
secondsMetric, secondsMetric,
totalsMetric, totalsMetric,
failedMetric,
// We don't show histogram in node graph at the moment but we could later add that into a node context menu. // We don't show histogram in node graph at the moment but we could later add that into a node context menu.
// 'traces_service_graph_request_seconds_bucket', // 'traces_service_graph_request_seconds_bucket',
// 'traces_service_graph_request_seconds_count', // 'traces_service_graph_request_seconds_count',
@ -147,14 +152,15 @@ export const serviceMapMetrics = [
* @param range * @param range
*/ */
export function mapPromMetricsToServiceMap(responses: DataQueryResponse[], range: TimeRange): [DataFrame, DataFrame] { export function mapPromMetricsToServiceMap(responses: DataQueryResponse[], range: TimeRange): [DataFrame, DataFrame] {
const [totalsDFView, secondsDFView] = getMetricFrames(responses); const frames = getMetricFrames(responses);
// First just collect data from the metrics into a map with nodes and edges as keys // First just collect data from the metrics into a map with nodes and edges as keys
const nodesMap: Record<string, any> = {}; const nodesMap: Record<string, ServiceMapStatistics> = {};
const edgesMap: Record<string, any> = {}; const edgesMap: Record<string, EdgeObject> = {};
// At this moment we don't have any error/success or other counts so we just use these 2 // At this moment we don't have any error/success or other counts so we just use these 2
collectMetricData(totalsDFView, 'total', totalsMetric, nodesMap, edgesMap); collectMetricData(frames[totalsMetric], 'total', totalsMetric, nodesMap, edgesMap);
collectMetricData(secondsDFView, 'seconds', secondsMetric, nodesMap, edgesMap); collectMetricData(frames[secondsMetric], 'seconds', secondsMetric, nodesMap, edgesMap);
collectMetricData(frames[failedMetric], 'failed', failedMetric, nodesMap, edgesMap);
return convertToDataFrames(nodesMap, edgesMap, range); return convertToDataFrames(nodesMap, edgesMap, range);
} }
@ -172,6 +178,14 @@ function createServiceMapDataFrames() {
name: Fields.secondaryStat, name: Fields.secondaryStat,
config: { unit: 'r/sec', displayName: 'Requests per second' }, config: { unit: 'r/sec', displayName: 'Requests per second' },
}, },
{
name: Fields.arc + 'success',
config: { displayName: 'Success', color: { fixedColor: 'green', mode: FieldColorModeId.Fixed } },
},
{
name: Fields.arc + 'failed',
config: { displayName: 'Failed', color: { fixedColor: 'red', mode: FieldColorModeId.Fixed } },
},
]); ]);
const edges = createDF('Edges', [ const edges = createDF('Edges', [
{ name: Fields.id }, { name: Fields.id },
@ -184,13 +198,29 @@ function createServiceMapDataFrames() {
return [nodes, edges]; return [nodes, edges];
} }
function getMetricFrames(responses: DataQueryResponse[]) { /**
const responsesMap = groupBy(responses[0].data, (data) => data.refId); * Group frames from response based on ref id which is set the same as the metric name so we know which metric is where
const totalsDFView = new DataFrameView(responsesMap[totalsMetric][0]); * and also put it into DataFrameView so it's easier to work with.
const secondsDFView = new DataFrameView(responsesMap[secondsMetric][0]); * @param responses
return [totalsDFView, secondsDFView]; */
function getMetricFrames(responses: DataQueryResponse[]): Record<string, DataFrameView> {
return responses[0].data.reduce<Record<string, DataFrameView>>((acc, frame) => {
acc[frame.refId] = new DataFrameView(frame);
return acc;
}, {});
} }
type ServiceMapStatistics = {
total?: number;
seconds?: number;
failed?: number;
};
type EdgeObject = ServiceMapStatistics & {
source: string;
target: string;
};
/** /**
* Collect data from a metric into a map of nodes and edges. The metric data is modeled as counts of metric per edge * Collect data from a metric into a map of nodes and edges. The metric data is modeled as counts of metric per edge
* which is a pair of client-server nodes. This means we convert each row of the metric 1-1 to edges and than we assign * which is a pair of client-server nodes. This means we convert each row of the metric 1-1 to edges and than we assign
@ -203,12 +233,16 @@ function getMetricFrames(responses: DataQueryResponse[]) {
* @param edgesMap * @param edgesMap
*/ */
function collectMetricData( function collectMetricData(
frame: DataFrameView, frame: DataFrameView | undefined,
stat: 'total' | 'seconds', stat: keyof ServiceMapStatistics,
metric: string, metric: string,
nodesMap: Record<string, any>, nodesMap: Record<string, ServiceMapStatistics>,
edgesMap: Record<string, any> edgesMap: Record<string, EdgeObject>
) { ) {
if (!frame) {
return;
}
// The name of the value column is in this format // The name of the value column is in this format
// TODO figure out if it can be changed // TODO figure out if it can be changed
const valueName = `Value #${metric}`; const valueName = `Value #${metric}`;
@ -218,24 +252,32 @@ function collectMetricData(
const edgeId = `${row.client}_${row.server}`; const edgeId = `${row.client}_${row.server}`;
if (!edgesMap[edgeId]) { if (!edgesMap[edgeId]) {
// Create edge as it does not exist yet
edgesMap[edgeId] = { edgesMap[edgeId] = {
target: row.server, target: row.server,
source: row.client, source: row.client,
[stat]: row[valueName], [stat]: row[valueName],
}; };
} else { } else {
// Add stat to edge
// We are adding the values if exists but that should not happen in general as there should be single row for
// an edge.
edgesMap[edgeId][stat] = (edgesMap[edgeId][stat] || 0) + row[valueName]; edgesMap[edgeId][stat] = (edgesMap[edgeId][stat] || 0) + row[valueName];
} }
if (!nodesMap[row.server]) { if (!nodesMap[row.server]) {
// Create node for server
nodesMap[row.server] = { nodesMap[row.server] = {
[stat]: row[valueName], [stat]: row[valueName],
}; };
} else { } else {
// Add stat to server node. Sum up values if there are multiple edges targeting this server node.
nodesMap[row.server][stat] = (nodesMap[row.server][stat] || 0) + row[valueName]; nodesMap[row.server][stat] = (nodesMap[row.server][stat] || 0) + row[valueName];
} }
if (!nodesMap[row.client]) { if (!nodesMap[row.client]) {
// Create the client node but don't add the stat as edge stats are attributed to the server node. This means for
// example that the number of requests in a node show how many requests it handled not how many it generated.
nodesMap[row.client] = { nodesMap[row.client] = {
[stat]: 0, [stat]: 0,
}; };
@ -244,8 +286,8 @@ function collectMetricData(
} }
function convertToDataFrames( function convertToDataFrames(
nodesMap: Record<string, any>, nodesMap: Record<string, ServiceMapStatistics>,
edgesMap: Record<string, any>, edgesMap: Record<string, EdgeObject>,
range: TimeRange range: TimeRange
): [DataFrame, DataFrame] { ): [DataFrame, DataFrame] {
const rangeMs = range.to.valueOf() - range.from.valueOf(); const rangeMs = range.to.valueOf() - range.from.valueOf();
@ -253,22 +295,24 @@ function convertToDataFrames(
for (const nodeId of Object.keys(nodesMap)) { for (const nodeId of Object.keys(nodesMap)) {
const node = nodesMap[nodeId]; const node = nodesMap[nodeId];
nodes.add({ nodes.add({
id: nodeId, [Fields.id]: nodeId,
title: nodeId, [Fields.title]: nodeId,
// NaN will not be shown in the node graph. This happens for a root client node which did not process // NaN will not be shown in the node graph. This happens for a root client node which did not process
// any requests itself. // any requests itself.
mainStat: node.total ? (node.seconds / node.total) * 1000 : Number.NaN, // Average response time [Fields.mainStat]: node.total ? (node.seconds! / node.total) * 1000 : Number.NaN, // Average response time
secondaryStat: node.total ? Math.round((node.total / (rangeMs / 1000)) * 100) / 100 : Number.NaN, // Request per second (to 2 decimals) [Fields.secondaryStat]: node.total ? Math.round((node.total / (rangeMs / 1000)) * 100) / 100 : Number.NaN, // Request per second (to 2 decimals)
[Fields.arc + 'success']: node.total ? (node.total - (node.failed || 0)) / node.total : 1,
[Fields.arc + 'failed']: node.total ? (node.failed || 0) / node.total : 0,
}); });
} }
for (const edgeId of Object.keys(edgesMap)) { for (const edgeId of Object.keys(edgesMap)) {
const edge = edgesMap[edgeId]; const edge = edgesMap[edgeId];
edges.add({ edges.add({
id: edgeId, [Fields.id]: edgeId,
source: edge.source, [Fields.source]: edge.source,
target: edge.target, [Fields.target]: edge.target,
mainStat: edge.total, // Requests [Fields.mainStat]: edge.total, // Requests
secondaryStat: edge.total ? (edge.seconds / edge.total) * 1000 : Number.NaN, // Average response time [Fields.secondaryStat]: edge.total ? (edge.seconds! / edge.total) * 1000 : Number.NaN, // Average response time
}); });
} }

Loading…
Cancel
Save