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);
});
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', () => {
const resp = [
{

View File

@ -799,9 +799,29 @@ 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
// fixed at some point.
nodes.refId = request.targets[0].refId;
edges.refId = request.targets[0].refId;
const { serviceMapIncludeNamespace, refId } = request.targets[0];
nodes.refId = refId;
edges.refId = refId;
if (serviceMapIncludeNamespace) {
nodes.fields[0].config = getFieldConfig(
datasourceUid, // datasourceUid
tempoDatasourceUid, // tempoDatasourceUid
'__data.fields.title', // targetField
'__data.fields[0]', // tempoField
undefined, // sourceField
{ targetNamespace: '__data.fields.subtitle' }
);
edges.fields[0].config = getFieldConfig(
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,
@ -815,6 +835,7 @@ function serviceMapQuery(request: DataQueryRequest<TempoQuery>, datasourceUid: s
'__data.fields.target',
'__data.fields.source'
);
}
return {
data: [nodes, edges],
@ -948,26 +969,42 @@ export function getFieldConfig(
tempoDatasourceUid: string,
targetField: 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 {
links: [
makePromLink(
'Request rate',
`sum by (client, server)(rate(${totalsMetric}{${sourceField}server="\${${targetField}}"}[$__rate_interval]))`,
`sum by (client, ${serverSumBy})(rate(${totalsMetric}{${source}${target}}[$__rate_interval]))`,
datasourceUid,
false
),
makePromLink(
'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,
false
),
makePromLink(
'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,
false
),

View File

@ -106,13 +106,17 @@ describe('mapPromMetricsToServiceMap', () => {
expect(edges.fields).toMatchObject([
{ name: 'id', values: ['app_db', 'lb_app'] },
{ name: 'source', values: ['app', 'lb'] },
{ name: 'sourceName', values: ['app', 'lb'] },
{ name: 'sourceNamespace', values: [undefined, undefined] },
{ name: 'target', values: ['db', 'app'] },
{ name: 'targetName', values: ['db', 'app'] },
{ name: 'targetNamespace', values: [undefined, undefined] },
{ name: 'mainstat', values: [1000, 2000] },
{ 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 = {
from: dateTime('2000-01-01T00:00:00'),
to: dateTime('2000-01-01T00:01:00'),
@ -137,7 +141,11 @@ describe('mapPromMetricsToServiceMap', () => {
expect(edges.fields).toMatchObject([
{ name: 'id', values: ['ns1/app_ns3/db', 'ns2/lb_ns1/app'] },
{ 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: 'targetName', values: ['db', 'app'] },
{ name: 'targetNamespace', values: ['ns3', 'ns1'] },
{ name: 'mainstat', values: [1000, 2000] },
{ name: 'secondarystat', values: [10, 20] },
]);

View File

@ -212,7 +212,11 @@ function createServiceMapDataFrames() {
const edges = createDF('Edges', [
{ name: Fields.id, 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: 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.secondaryStat,
@ -249,9 +253,22 @@ type NodeObject = ServiceMapStatistics & {
type EdgeObject = ServiceMapStatistics & {
source: string;
sourceName: string;
sourceNamespace: 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
* 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
edgesMap[edgeId] = {
target: serverId,
targetName: row.server,
targetNamespace: row.server_service_namespace,
source: clientId,
sourceName: row.client,
sourceNamespace: row.client_service_namespace,
[stat]: row[valueName],
};
} else {
@ -348,7 +369,11 @@ function convertToDataFrames(
edges.add({
[Fields.id]: edgeId,
[Fields.source]: edge.source,
[AdditionalEdgeFields.sourceName]: edge.sourceName,
[AdditionalEdgeFields.sourceNamespace]: edge.sourceNamespace,
[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.secondaryStat]: edge.total ? Math.round(edge.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals)
});