Tempo Service Map: Fix context menu links in service map when namespace is present (#74186)

* Add necessary fields to edges

* Add information about name and namespace to edge when available

* Use new fields to build search taking into account namespace

* Remove new fields from NodeGraphDataFrameFieldNames, define them locally
This commit is contained in:
Javier Ruiz 2023-09-13 10:17:31 +02:00 committed by GitHub
parent 0c44a6f9bb
commit 3bae1c564d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 21 deletions

View File

@ -695,6 +695,87 @@ describe('Tempo service graph view', () => {
expect(fieldConfig).toStrictEqual(resultObj); expect(fieldConfig).toStrictEqual(resultObj);
}); });
it('should get field config correctly when namespaces are present', () => {
let datasourceUid = 's4Jvz8Qnk';
let tempoDatasourceUid = 'EbPO1fYnz';
let targetField = '__data.fields.targetName';
let tempoField = '__data.fields.target';
let sourceField = '__data.fields.sourceName';
let namespaceFields = {
targetNamespace: '__data.fields.targetNamespace',
sourceNamespace: '__data.fields.sourceNamespace',
};
let fieldConfig = getFieldConfig(
datasourceUid,
tempoDatasourceUid,
targetField,
tempoField,
sourceField,
namespaceFields
);
let resultObj = {
links: [
{
url: '',
title: 'Request rate',
internal: {
query: {
expr: 'sum by (client, server, server_service_namespace, client_service_namespace)(rate(traces_service_graph_request_total{client="${__data.fields.sourceName}",client_service_namespace="${__data.fields.sourceNamespace}",server="${__data.fields.targetName}",server_service_namespace="${__data.fields.targetNamespace}"}[$__rate_interval]))',
range: true,
exemplar: true,
instant: false,
},
datasourceUid: 's4Jvz8Qnk',
datasourceName: '',
},
},
{
url: '',
title: 'Request histogram',
internal: {
query: {
expr: 'histogram_quantile(0.9, sum(rate(traces_service_graph_request_server_seconds_bucket{client="${__data.fields.sourceName}",client_service_namespace="${__data.fields.sourceNamespace}",server="${__data.fields.targetName}",server_service_namespace="${__data.fields.targetNamespace}"}[$__rate_interval])) by (le, client, server, server_service_namespace, client_service_namespace))',
range: true,
exemplar: true,
instant: false,
},
datasourceUid: 's4Jvz8Qnk',
datasourceName: '',
},
},
{
url: '',
title: 'Failed request rate',
internal: {
query: {
expr: 'sum by (client, server, server_service_namespace, client_service_namespace)(rate(traces_service_graph_request_failed_total{client="${__data.fields.sourceName}",client_service_namespace="${__data.fields.sourceNamespace}",server="${__data.fields.targetName}",server_service_namespace="${__data.fields.targetNamespace}"}[$__rate_interval]))',
range: true,
exemplar: true,
instant: false,
},
datasourceUid: 's4Jvz8Qnk',
datasourceName: '',
},
},
{
url: '',
title: 'View traces',
internal: {
query: {
queryType: 'nativeSearch',
serviceName: '${__data.fields.target}',
},
datasourceUid: 'EbPO1fYnz',
datasourceName: '',
},
},
],
};
expect(fieldConfig).toStrictEqual(resultObj);
});
it('should get rate aligned values correctly', () => { it('should get rate aligned values correctly', () => {
const resp = [ const resp = [
{ {

View File

@ -799,22 +799,43 @@ function serviceMapQuery(request: DataQueryRequest<TempoQuery>, datasourceUid: s
// No handling of multiple targets assume just one. NodeGraph does not support it anyway, but still should be // No handling of multiple targets assume just one. NodeGraph does not support it anyway, but still should be
// fixed at some point. // fixed at some point.
nodes.refId = request.targets[0].refId; const { serviceMapIncludeNamespace, refId } = request.targets[0];
edges.refId = request.targets[0].refId; nodes.refId = refId;
edges.refId = refId;
nodes.fields[0].config = getFieldConfig( if (serviceMapIncludeNamespace) {
datasourceUid, nodes.fields[0].config = getFieldConfig(
tempoDatasourceUid, datasourceUid, // datasourceUid
'__data.fields.id', tempoDatasourceUid, // tempoDatasourceUid
'__data.fields[0]' '__data.fields.title', // targetField
); '__data.fields[0]', // tempoField
edges.fields[0].config = getFieldConfig( undefined, // sourceField
datasourceUid, { targetNamespace: '__data.fields.subtitle' }
tempoDatasourceUid, );
'__data.fields.target',
'__data.fields.target', edges.fields[0].config = getFieldConfig(
'__data.fields.source' datasourceUid, // datasourceUid
); tempoDatasourceUid, // tempoDatasourceUid
'__data.fields.targetName', // targetField
'__data.fields.target', // tempoField
'__data.fields.sourceName', // sourceField
{ targetNamespace: '__data.fields.targetNamespace', sourceNamespace: '__data.fields.sourceNamespace' }
);
} else {
nodes.fields[0].config = getFieldConfig(
datasourceUid,
tempoDatasourceUid,
'__data.fields.id',
'__data.fields[0]'
);
edges.fields[0].config = getFieldConfig(
datasourceUid,
tempoDatasourceUid,
'__data.fields.target',
'__data.fields.target',
'__data.fields.source'
);
}
return { return {
data: [nodes, edges], data: [nodes, edges],
@ -948,26 +969,42 @@ export function getFieldConfig(
tempoDatasourceUid: string, tempoDatasourceUid: string,
targetField: string, targetField: string,
tempoField: string, tempoField: string,
sourceField?: string sourceField?: string,
namespaceFields?: { targetNamespace: string; sourceNamespace?: string }
) { ) {
sourceField = sourceField ? `client="\${${sourceField}}",` : ''; let source = sourceField ? `client="\${${sourceField}}",` : '';
let target = `server="\${${targetField}}"`;
let serverSumBy = 'server';
if (namespaceFields !== undefined) {
const { targetNamespace } = namespaceFields;
target += `,server_service_namespace="\${${targetNamespace}}"`;
serverSumBy += ', server_service_namespace';
if (source) {
const { sourceNamespace } = namespaceFields;
source += `client_service_namespace="\${${sourceNamespace}}",`;
serverSumBy += ', client_service_namespace';
}
}
return { return {
links: [ links: [
makePromLink( makePromLink(
'Request rate', 'Request rate',
`sum by (client, server)(rate(${totalsMetric}{${sourceField}server="\${${targetField}}"}[$__rate_interval]))`, `sum by (client, ${serverSumBy})(rate(${totalsMetric}{${source}${target}}[$__rate_interval]))`,
datasourceUid, datasourceUid,
false false
), ),
makePromLink( makePromLink(
'Request histogram', 'Request histogram',
`histogram_quantile(0.9, sum(rate(${histogramMetric}{${sourceField}server="\${${targetField}}"}[$__rate_interval])) by (le, client, server))`, `histogram_quantile(0.9, sum(rate(${histogramMetric}{${source}${target}}[$__rate_interval])) by (le, client, ${serverSumBy}))`,
datasourceUid, datasourceUid,
false false
), ),
makePromLink( makePromLink(
'Failed request rate', 'Failed request rate',
`sum by (client, server)(rate(${failedMetric}{${sourceField}server="\${${targetField}}"}[$__rate_interval]))`, `sum by (client, ${serverSumBy})(rate(${failedMetric}{${source}${target}}[$__rate_interval]))`,
datasourceUid, datasourceUid,
false false
), ),

View File

@ -106,13 +106,17 @@ describe('mapPromMetricsToServiceMap', () => {
expect(edges.fields).toMatchObject([ expect(edges.fields).toMatchObject([
{ name: 'id', values: ['app_db', 'lb_app'] }, { name: 'id', values: ['app_db', 'lb_app'] },
{ name: 'source', values: ['app', 'lb'] }, { name: 'source', values: ['app', 'lb'] },
{ name: 'sourceName', values: ['app', 'lb'] },
{ name: 'sourceNamespace', values: [undefined, undefined] },
{ name: 'target', values: ['db', 'app'] }, { name: 'target', values: ['db', 'app'] },
{ name: 'targetName', values: ['db', 'app'] },
{ name: 'targetNamespace', values: [undefined, undefined] },
{ name: 'mainstat', values: [1000, 2000] }, { name: 'mainstat', values: [1000, 2000] },
{ name: 'secondarystat', values: [10, 20] }, { name: 'secondarystat', values: [10, 20] },
]); ]);
}); });
it('transforms prom metrics to service graph inlucding namespace', async () => { it('transforms prom metrics to service graph including namespace', async () => {
const range = { const range = {
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'),
@ -137,7 +141,11 @@ describe('mapPromMetricsToServiceMap', () => {
expect(edges.fields).toMatchObject([ expect(edges.fields).toMatchObject([
{ name: 'id', values: ['ns1/app_ns3/db', 'ns2/lb_ns1/app'] }, { name: 'id', values: ['ns1/app_ns3/db', 'ns2/lb_ns1/app'] },
{ name: 'source', values: ['ns1/app', 'ns2/lb'] }, { name: 'source', values: ['ns1/app', 'ns2/lb'] },
{ name: 'sourceName', values: ['app', 'lb'] },
{ name: 'sourceNamespace', values: ['ns1', 'ns2'] },
{ name: 'target', values: ['ns3/db', 'ns1/app'] }, { name: 'target', values: ['ns3/db', 'ns1/app'] },
{ name: 'targetName', values: ['db', 'app'] },
{ name: 'targetNamespace', values: ['ns3', 'ns1'] },
{ name: 'mainstat', values: [1000, 2000] }, { name: 'mainstat', values: [1000, 2000] },
{ name: 'secondarystat', values: [10, 20] }, { name: 'secondarystat', values: [10, 20] },
]); ]);

View File

@ -212,7 +212,11 @@ function createServiceMapDataFrames() {
const edges = createDF('Edges', [ const edges = createDF('Edges', [
{ name: Fields.id, type: FieldType.string }, { name: Fields.id, type: FieldType.string },
{ name: Fields.source, type: FieldType.string }, { name: Fields.source, type: FieldType.string },
{ name: AdditionalEdgeFields.sourceName, type: FieldType.string },
{ name: AdditionalEdgeFields.sourceNamespace, type: FieldType.string },
{ name: Fields.target, type: FieldType.string }, { name: Fields.target, type: FieldType.string },
{ name: AdditionalEdgeFields.targetName, type: FieldType.string },
{ name: AdditionalEdgeFields.targetNamespace, type: FieldType.string },
{ name: Fields.mainStat, type: FieldType.number, config: { unit: 'ms/r', displayName: 'Average response time' } }, { name: Fields.mainStat, type: FieldType.number, config: { unit: 'ms/r', displayName: 'Average response time' } },
{ {
name: Fields.secondaryStat, name: Fields.secondaryStat,
@ -249,9 +253,22 @@ type NodeObject = ServiceMapStatistics & {
type EdgeObject = ServiceMapStatistics & { type EdgeObject = ServiceMapStatistics & {
source: string; source: string;
sourceName: string;
sourceNamespace: string;
target: string; target: string;
targetName: string;
targetNamespace: string;
}; };
// These fields are not necessary for rendering, so not available from the Fields enum
// Will be used for linking when namespace is present
enum AdditionalEdgeFields {
sourceName = 'sourceName',
sourceNamespace = 'sourceNamespace',
targetName = 'targetName',
targetNamespace = 'targetNamespace',
}
/** /**
* 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
@ -289,7 +306,11 @@ function collectMetricData(
// Create edge as it does not exist yet // Create edge as it does not exist yet
edgesMap[edgeId] = { edgesMap[edgeId] = {
target: serverId, target: serverId,
targetName: row.server,
targetNamespace: row.server_service_namespace,
source: clientId, source: clientId,
sourceName: row.client,
sourceNamespace: row.client_service_namespace,
[stat]: row[valueName], [stat]: row[valueName],
}; };
} else { } else {
@ -348,7 +369,11 @@ function convertToDataFrames(
edges.add({ edges.add({
[Fields.id]: edgeId, [Fields.id]: edgeId,
[Fields.source]: edge.source, [Fields.source]: edge.source,
[AdditionalEdgeFields.sourceName]: edge.sourceName,
[AdditionalEdgeFields.sourceNamespace]: edge.sourceNamespace,
[Fields.target]: edge.target, [Fields.target]: edge.target,
[AdditionalEdgeFields.targetName]: edge.targetName,
[AdditionalEdgeFields.targetNamespace]: edge.targetNamespace,
[Fields.mainStat]: edge.total ? (edge.seconds! / edge.total) * 1000 : Number.NaN, // Average response time [Fields.mainStat]: edge.total ? (edge.seconds! / edge.total) * 1000 : Number.NaN, // Average response time
[Fields.secondaryStat]: edge.total ? Math.round(edge.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals) [Fields.secondaryStat]: edge.total ? Math.round(edge.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals)
}); });