Tempo: Add error metric to service graph (#40774)
* Add typings * Query error metric for service graph
This commit is contained in:
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user