NodeGraph: Improve view traces for uninstrumented services (#98442)

* NodeGraph: Improve view traces for uninstrumented services

* Switch to onBuildUrl and more peer attributes

* Update unit tests

* Added test for new logic

* Open traces in same tab

* Update the tests

* bring back internal link

* Update public/app/plugins/datasource/tempo/datasource.ts

Co-authored-by: Joey <90795735+joey-grafana@users.noreply.github.com>

* Revert export of generateInternalHref

* Update tests after change from onBuildUrl to query function

---------

Co-authored-by: Domas Lapinskas <domasx2@gmail.com>
Co-authored-by: Joey <90795735+joey-grafana@users.noreply.github.com>
This commit is contained in:
Edvard Falkskär 2025-02-12 10:28:44 +01:00 committed by GitHub
parent a0701a42f1
commit eb4c428d4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 333 additions and 86 deletions

View File

@ -1,3 +1,4 @@
import { ScopedVars } from './ScopedVars';
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore'; import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
import { InterpolateFunction } from './panel'; import { InterpolateFunction } from './panel';
import { DataQuery } from './query'; import { DataQuery } from './query';
@ -80,7 +81,7 @@ export interface DataLinkTransformationConfig {
/** @internal */ /** @internal */
export interface InternalDataLink<T extends DataQuery = any> { export interface InternalDataLink<T extends DataQuery = any> {
query: T; query: T | ((options: { replaceVariables: InterpolateFunction; scopedVars: ScopedVars }) => T);
datasourceUid: string; datasourceUid: string;
datasourceName: string; // used as a title if `DataLink.title` is empty datasourceName: string; // used as a title if `DataLink.title` is empty
panelsState?: ExplorePanelsState; panelsState?: ExplorePanelsState;

View File

@ -38,7 +38,11 @@ export type LinkToExploreOptions = {
export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> { export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> {
const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options; const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options;
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables); const query =
typeof link.internal?.query === 'function'
? link.internal.query({ replaceVariables, scopedVars })
: internalLink.query;
const interpolatedQuery = interpolateObject(query, scopedVars, replaceVariables);
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables); const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables); const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables);
const title = link.title ? link.title : internalLink.datasourceName; const title = link.title ? link.title : internalLink.datasourceName;

View File

@ -44,4 +44,7 @@ export enum NodeGraphDataFrameFieldNames {
// Supplies a fixed Y position for the node to have in the finished graph. // Supplies a fixed Y position for the node to have in the finished graph.
fixedY = 'fixedy', fixedY = 'fixedy',
// Whether the node is instrumented or not
isInstrumented = 'isinstrumented',
} }

View File

@ -15,6 +15,8 @@ import {
DataQueryRequest, DataQueryRequest,
getTimeZone, getTimeZone,
PluginMetaInfo, PluginMetaInfo,
DataLink,
NodeGraphDataFrameFieldNames,
} from '@grafana/data'; } from '@grafana/data';
import { import {
BackendDataSourceResponse, BackendDataSourceResponse,
@ -544,6 +546,32 @@ describe('Tempo service graph view', () => {
expect(response.data[1].fields[0]?.config?.links?.length).toBeGreaterThan(0); expect(response.data[1].fields[0]?.config?.links?.length).toBeGreaterThan(0);
expect(response.data[1].fields[0]?.config?.links).toEqual(serviceGraphLinks); expect(response.data[1].fields[0]?.config?.links).toEqual(serviceGraphLinks);
const viewServicesLink = response.data[1].fields[0]?.config?.links.find(
(link: DataLink) => link.title === 'View traces'
);
expect(viewServicesLink).toBeDefined();
expect(viewServicesLink.internal.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({
refId: 'A',
queryType: 'traceqlSearch',
filters: [
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: 'my-service',
valueType: 'string',
},
],
});
expect(viewServicesLink.internal.query({ replaceVariables: replaceVariablesUninstrumented })).toEqual({
refId: 'A',
queryType: 'traceql',
filters: [],
query:
'{span.db.name="my-service" || span.db.system="my-service" || span.peer.service="my-service" || span.messaging.system="my-service" || span.net.peer.name="my-service"}',
});
expect(response.data[2].name).toBe('Edges'); expect(response.data[2].name).toBe('Edges');
expect(response.data[2].fields[0].values.length).toBe(2); expect(response.data[2].fields[0].values.length).toBe(2);
}); });
@ -584,14 +612,26 @@ describe('Tempo service graph view', () => {
'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range]))'
); );
expect(nthQuery(0).targets[1].expr).toBe( expect(nthQuery(0).targets[1].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range]))' 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{ foo="bar" })'
); );
expect(nthQuery(0).targets[2].expr).toBe( expect(nthQuery(0).targets[2].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range]))'
); );
expect(nthQuery(0).targets[3].expr).toBe( expect(nthQuery(0).targets[3].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_total{ foo="bar" })'
);
expect(nthQuery(0).targets[4].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range]))'
);
expect(nthQuery(0).targets[5].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_failed_total{ foo="bar" })'
);
expect(nthQuery(0).targets[6].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range]))'
); );
expect(nthQuery(0).targets[7].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{ foo="bar" })'
);
}); });
it('runs correct queries with multiple serviceMapQuery defined', async () => { it('runs correct queries with multiple serviceMapQuery defined', async () => {
@ -632,14 +672,26 @@ describe('Tempo service graph view', () => {
'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{baz="bad"}[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_sum{baz="bad"}[$__range]))'
); );
expect(nthQuery(0).targets[1].expr).toBe( expect(nthQuery(0).targets[1].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_total{baz="bad"}[$__range]))' 'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_server_seconds_sum{baz="bad"})'
); );
expect(nthQuery(0).targets[2].expr).toBe( expect(nthQuery(0).targets[2].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_failed_total{baz="bad"}[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_total{baz="bad"}[$__range]))'
); );
expect(nthQuery(0).targets[3].expr).toBe( expect(nthQuery(0).targets[3].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_total{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_total{baz="bad"})'
);
expect(nthQuery(0).targets[4].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_failed_total{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_failed_total{baz="bad"}[$__range]))'
);
expect(nthQuery(0).targets[5].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_failed_total{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_failed_total{baz="bad"})'
);
expect(nthQuery(0).targets[6].expr).toBe(
'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{baz="bad"}[$__range]))' 'sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{ foo="bar" }[$__range])) OR sum by (client, server) (rate(traces_service_graph_request_server_seconds_bucket{baz="bad"}[$__range]))'
); );
expect(nthQuery(0).targets[7].expr).toBe(
'group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{ foo="bar" }) OR group by (client, connection_type, server) (traces_service_graph_request_server_seconds_bucket{baz="bad"})'
);
}); });
it('should build expr correctly', () => { it('should build expr correctly', () => {
@ -803,27 +855,33 @@ describe('Tempo service graph view', () => {
url: '', url: '',
title: 'View traces', title: 'View traces',
internal: { internal: {
query: {
refId: 'A',
queryType: 'traceqlSearch',
filters: [
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: '${__data.fields.target}',
valueType: 'string',
},
],
},
datasourceUid: 'EbPO1fYnz',
datasourceName: '', datasourceName: '',
datasourceUid: 'EbPO1fYnz',
query: expect.any(Function),
}, },
}, },
], ],
}; };
expect(fieldConfig).toStrictEqual(resultObj); expect(fieldConfig).toStrictEqual(resultObj);
const viewServicesLink: DataLink | undefined = fieldConfig.links.find(
(link: DataLink) => link.title === 'View traces'
);
expect(viewServicesLink).toBeDefined();
expect(viewServicesLink!.internal!.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({
refId: 'A',
queryType: 'traceqlSearch',
filters: [
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: 'my-service',
valueType: 'string',
},
],
});
}); });
it('should get field config correctly when namespaces are present', () => { it('should get field config correctly when namespaces are present', () => {
@ -894,35 +952,41 @@ describe('Tempo service graph view', () => {
url: '', url: '',
title: 'View traces', title: 'View traces',
internal: { internal: {
query: {
queryType: 'traceqlSearch',
refId: 'A',
filters: [
{
id: 'service-namespace',
operator: '=',
scope: 'resource',
tag: 'service.namespace',
value: '${__data.fields.targetNamespace}',
valueType: 'string',
},
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: '${__data.fields.targetName}',
valueType: 'string',
},
],
},
datasourceUid: 'EbPO1fYnz',
datasourceName: '', datasourceName: '',
datasourceUid: 'EbPO1fYnz',
query: expect.any(Function),
}, },
}, },
], ],
}; };
expect(fieldConfig).toStrictEqual(resultObj); expect(fieldConfig).toStrictEqual(resultObj);
const viewServicesLink: DataLink | undefined = fieldConfig.links.find(
(link: DataLink) => link.title === 'View traces'
);
expect(viewServicesLink).toBeDefined();
expect(viewServicesLink!.internal!.query({ replaceVariables: replaceVariablesInstrumented })).toEqual({
refId: 'A',
queryType: 'traceqlSearch',
filters: [
{
id: 'service-namespace',
operator: '=',
scope: 'resource',
tag: 'service.namespace',
value: 'my-namespace',
valueType: 'string',
},
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: 'my-service',
valueType: 'string',
},
],
});
}); });
it('should get rate aligned values correctly', () => { it('should get rate aligned values correctly', () => {
@ -1435,26 +1499,31 @@ const serviceGraphLinks = [
url: '', url: '',
title: 'View traces', title: 'View traces',
internal: { internal: {
query: { query: expect.any(Function),
refId: 'A',
queryType: 'traceqlSearch',
filters: [
{
id: 'service-name',
operator: '=',
scope: 'resource',
tag: 'service.name',
value: '${__data.fields.id}',
valueType: 'string',
},
],
} as TempoQuery,
datasourceUid: 'gdev-tempo', datasourceUid: 'gdev-tempo',
datasourceName: 'Tempo', datasourceName: 'Tempo',
}, },
}, },
]; ];
const replaceVariablesInstrumented = (variable: string): string => {
const variables: Record<string, string> = {
[`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`]: 'my-service',
[`\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`]: 'my-namespace',
[`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`]: 'true',
};
return variables[variable];
};
const replaceVariablesUninstrumented = (variable: string): string => {
const variables: Record<string, string> = {
[`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`]: 'my-service',
[`\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`]: 'my-namespace',
[`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`]: 'false',
};
return variables[variable];
};
interface PromQuery extends DataQuery { interface PromQuery extends DataQuery {
expr: string; expr: string;
} }

View File

@ -7,6 +7,7 @@ import {
CoreApp, CoreApp,
DataFrame, DataFrame,
DataFrameDTO, DataFrameDTO,
DataLink,
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
DataQueryResponseData, DataQueryResponseData,
@ -15,6 +16,7 @@ import {
dateTime, dateTime,
FieldType, FieldType,
LoadingState, LoadingState,
NodeGraphDataFrameFieldNames,
rangeUtil, rangeUtil,
ScopedVars, ScopedVars,
SelectableValue, SelectableValue,
@ -1122,13 +1124,7 @@ export function getFieldConfig(
datasourceUid, datasourceUid,
false false
), ),
makeTempoLink( makeTempoLinkServiceMap('View traces', tempoDatasourceUid, !!namespaceFields?.targetNamespace),
'View traces',
namespaceFields !== undefined ? `\${${namespaceFields.targetNamespace}}` : '',
`\${${targetField}}`,
'',
tempoDatasourceUid
),
], ],
}; };
} }
@ -1183,25 +1179,99 @@ export function makeTempoLink(
}; };
} }
function makeTempoLinkServiceMap(
title: string,
datasourceUid: string,
includeNamespace: boolean
): DataLink<TempoQuery> {
return {
url: '',
title,
internal: {
datasourceUid,
datasourceName: getDataSourceSrv().getInstanceSettings(datasourceUid)?.name ?? '',
query: ({ replaceVariables, scopedVars }) => {
const serviceName = replaceVariables?.(`\${__data.fields.${NodeGraphDataFrameFieldNames.title}}`, scopedVars);
const serviceNamespace = replaceVariables?.(
`\${__data.fields.${NodeGraphDataFrameFieldNames.subTitle}}`,
scopedVars
);
const isInstrumented =
replaceVariables?.(`\${__data.fields.${NodeGraphDataFrameFieldNames.isInstrumented}}`, scopedVars) !==
'false';
const query: TempoQuery = { refId: 'A', queryType: 'traceqlSearch', filters: [] };
// Only do the peer query if service is actively set as not instrumented
if (isInstrumented === false) {
const filters = ['db.name', 'db.system', 'peer.service', 'messaging.system', 'net.peer.name']
.map((peerAttribute) => `span.${peerAttribute}="${serviceName}"`)
.join(' || ');
query.queryType = 'traceql';
query.query = `{${filters}}`;
} else {
if (includeNamespace && serviceNamespace) {
query.filters.push({
id: 'service-namespace',
scope: TraceqlSearchScope.Resource,
tag: 'service.namespace',
value: serviceNamespace,
operator: '=',
valueType: 'string',
});
}
if (serviceName) {
query.filters.push({
id: 'service-name',
scope: TraceqlSearchScope.Resource,
tag: 'service.name',
value: serviceName,
operator: '=',
valueType: 'string',
});
}
}
return query;
},
},
};
}
function makePromServiceMapRequest(options: DataQueryRequest<TempoQuery>): DataQueryRequest<PromQuery> { function makePromServiceMapRequest(options: DataQueryRequest<TempoQuery>): DataQueryRequest<PromQuery> {
return { return {
...options, ...options,
targets: serviceMapMetrics.map((metric) => { targets: serviceMapMetrics
const { serviceMapQuery, serviceMapIncludeNamespace: serviceMapIncludeNamespace } = options.targets[0]; .map<PromQuery[]>((metric) => {
const extraSumByFields = serviceMapIncludeNamespace ? ', client_service_namespace, server_service_namespace' : ''; const { serviceMapQuery, serviceMapIncludeNamespace: serviceMapIncludeNamespace } = options.targets[0];
const queries = Array.isArray(serviceMapQuery) ? serviceMapQuery : [serviceMapQuery]; const extraSumByFields = serviceMapIncludeNamespace
const subExprs = queries.map( ? ', client_service_namespace, server_service_namespace'
(query) => `sum by (client, server${extraSumByFields}) (rate(${metric}${query || ''}[$__range]))` : '';
); const queries = Array.isArray(serviceMapQuery) ? serviceMapQuery : [serviceMapQuery];
return { const sumSubExprs = queries.map(
format: 'table', (query) => `sum by (client, server${extraSumByFields}) (rate(${metric}${query || ''}[$__range]))`
refId: metric, );
// options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for const groupSubExprs = queries.map(
// service map at the same time anyway (query) => `group by (client, connection_type, server${extraSumByFields}) (${metric}${query || ''})`
expr: subExprs.join(' OR '), );
instant: true,
}; return [
}), {
format: 'table',
refId: metric,
// options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for
// service map at the same time anyway
expr: sumSubExprs.join(' OR '),
instant: true,
},
{
format: 'table',
refId: `${metric}_labels`,
expr: groupSubExprs.join(' OR '),
instant: true,
},
];
})
.flat(),
}; };
} }

View File

@ -20,6 +20,7 @@ it('assigns correct field type even if values are numbers', async () => {
{ name: 'secondarystat', values: [10, 20], type: FieldType.number }, { name: 'secondarystat', values: [10, 20], type: FieldType.number },
{ name: 'arc__success', values: [1, 1], type: FieldType.number }, { name: 'arc__success', values: [1, 1], type: FieldType.number },
{ name: 'arc__failed', values: [0, 0], type: FieldType.number }, { name: 'arc__failed', values: [0, 0], type: FieldType.number },
{ name: 'isinstrumented', values: [true, true], type: FieldType.boolean },
]); ]);
}); });
@ -41,6 +42,7 @@ it('do not fail on response with empty list', async () => {
{ name: 'secondarystat', values: [], type: FieldType.number }, { name: 'secondarystat', values: [], type: FieldType.number },
{ name: 'arc__success', values: [], type: FieldType.number }, { name: 'arc__success', values: [], type: FieldType.number },
{ name: 'arc__failed', values: [], type: FieldType.number }, { name: 'arc__failed', values: [], type: FieldType.number },
{ name: 'isinstrumented', values: [], type: FieldType.boolean },
]); ]);
}); });
@ -66,6 +68,7 @@ describe('mapPromMetricsToServiceMap', () => {
{ name: 'secondarystat', values: [10, 20, NaN] }, { name: 'secondarystat', values: [10, 20, NaN] },
{ name: 'arc__success', values: [0.8, 0.25, 1] }, { name: 'arc__success', values: [0.8, 0.25, 1] },
{ name: 'arc__failed', values: [0.2, 0.75, 0] }, { name: 'arc__failed', values: [0.2, 0.75, 0] },
{ name: 'isinstrumented', values: [true, true, true] },
]); ]);
expect(edges.fields).toMatchObject([ expect(edges.fields).toMatchObject([
{ name: 'id', values: ['app_db', 'lb_app'] }, { name: 'id', values: ['app_db', 'lb_app'] },
@ -101,6 +104,7 @@ describe('mapPromMetricsToServiceMap', () => {
{ name: 'secondarystat', values: [10, 20, NaN] }, { name: 'secondarystat', values: [10, 20, NaN] },
{ name: 'arc__success', values: [0.8, 0.25, 1] }, { name: 'arc__success', values: [0.8, 0.25, 1] },
{ name: 'arc__failed', values: [0.2, 0.75, 0] }, { name: 'arc__failed', values: [0.2, 0.75, 0] },
{ name: 'isinstrumented', values: [true, true, true] },
]); ]);
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'] },
@ -138,6 +142,41 @@ describe('mapPromMetricsToServiceMap', () => {
{ name: 'secondarystat', values: [10, 20, NaN] }, { name: 'secondarystat', values: [10, 20, NaN] },
{ name: 'arc__success', values: [0, 0, 1] }, { name: 'arc__success', values: [0, 0, 1] },
{ name: 'arc__failed', values: [1, 1, 0] }, { name: 'arc__failed', values: [1, 1, 0] },
{ name: 'isinstrumented', values: [true, true, true] },
]);
});
it('handles setting isInstrumented based on the connection_type', () => {
const range = {
from: dateTime('2000-01-01T00:00:00'),
to: dateTime('2000-01-01T00:01:00'),
};
const { nodes } = mapPromMetricsToServiceMap(
[
{
data: [
totalsPromMetric(true),
secondsPromMetric(true),
secondsLabelsPromMetric(true),
failedPromMetric(true),
],
},
],
{
...range,
raw: range,
}
);
expect(nodes.fields).toMatchObject([
{ name: 'id', values: ['ns3/db', 'ns1/app', 'ns2/lb'] },
{ name: 'title', values: ['db', 'app', 'lb'] },
{ name: 'subtitle', values: ['ns3', 'ns1', 'ns2'] },
{ name: 'mainstat', values: [1000, 2000, NaN] },
{ name: 'secondarystat', values: [10, 20, NaN] },
{ name: 'arc__success', values: [0.8, 0.25, 1] },
{ name: 'arc__failed', values: [0.2, 0.75, 0] },
{ name: 'isinstrumented', values: [true, false, true] },
]); ]);
}); });
}); });
@ -182,6 +221,27 @@ const secondsPromMetric = (namespace?: boolean) =>
], ],
}); });
const secondsLabelsPromMetric = (namespace?: boolean) =>
createDataFrame({
refId: 'traces_service_graph_request_server_seconds_sum_labels',
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_server_seconds_sum_label', values: [1, 1] },
{ name: 'connection_type', values: ['messaging_system', 'virtual_node'] },
...(namespace
? [
{ name: 'client_service_namespace', values: ['ns1', 'ns2'] },
{ name: 'server_service_namespace', values: ['ns3', 'ns1'] },
]
: []),
],
});
const failedPromMetric = (namespace?: boolean) => const failedPromMetric = (namespace?: boolean) =>
createDataFrame({ createDataFrame({
refId: 'traces_service_graph_request_failed_total', refId: 'traces_service_graph_request_failed_total',

View File

@ -61,6 +61,10 @@ export function mapPromMetricsToServiceMap(
collectMetricData(frames[secondsMetric], 'seconds', secondsMetric, nodesMap, edgesMap); collectMetricData(frames[secondsMetric], 'seconds', secondsMetric, nodesMap, edgesMap);
collectMetricData(frames[failedMetric], 'failed', failedMetric, nodesMap, edgesMap); collectMetricData(frames[failedMetric], 'failed', failedMetric, nodesMap, edgesMap);
collectIsInstrumented(frames[`${totalsMetric}_labels`], nodesMap);
collectIsInstrumented(frames[`${secondsMetric}_labels`], nodesMap);
collectIsInstrumented(frames[`${failedMetric}_labels`], nodesMap);
return convertToDataFrames(nodesMap, edgesMap, range); return convertToDataFrames(nodesMap, edgesMap, range);
} }
@ -97,6 +101,11 @@ function createServiceMapDataFrames() {
config: { displayName: 'Failed', color: { fixedColor: 'red', mode: FieldColorModeId.Fixed } }, config: { displayName: 'Failed', color: { fixedColor: 'red', mode: FieldColorModeId.Fixed } },
values: [], values: [],
}, },
{
name: Fields.isInstrumented,
type: FieldType.boolean,
values: [],
},
]); ]);
const edges = createDF('Edges', [ const edges = createDF('Edges', [
{ name: Fields.id, type: FieldType.string, values: [] }, { name: Fields.id, type: FieldType.string, values: [] },
@ -145,6 +154,7 @@ type ServiceMapStatistics = {
type NodeObject = ServiceMapStatistics & { type NodeObject = ServiceMapStatistics & {
name: string; name: string;
namespace?: string; namespace?: string;
isInstrumented?: boolean;
}; };
type EdgeObject = ServiceMapStatistics & { type EdgeObject = ServiceMapStatistics & {
@ -240,6 +250,21 @@ function collectMetricData(
} }
} }
function collectIsInstrumented(frame: DataFrameView | undefined, nodesMap: Record<string, NodeObject>) {
if (!frame) {
return;
}
for (let i = 0; i < frame.length; i++) {
const row = frame.get(i);
const serverId = row.server_service_namespace ? `${row.server_service_namespace}/${row.server}` : row.server;
if (nodesMap[serverId] && nodesMap[serverId].isInstrumented !== true) {
nodesMap[serverId].isInstrumented = row.connection_type === '' || row.connection_type === 'messaging_system';
}
}
}
function convertToDataFrames( function convertToDataFrames(
nodesMap: Record<string, NodeObject>, nodesMap: Record<string, NodeObject>,
edgesMap: Record<string, EdgeObject>, edgesMap: Record<string, EdgeObject>,
@ -258,6 +283,7 @@ function convertToDataFrames(
[Fields.secondaryStat]: node.total ? Math.round(node.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals) [Fields.secondaryStat]: node.total ? Math.round(node.total * 100) / 100 : Number.NaN, // Request per second (to 2 decimals)
[Fields.arc + 'success']: node.total ? (node.total - Math.min(node.failed || 0, node.total)) / node.total : 1, [Fields.arc + 'success']: node.total ? (node.total - Math.min(node.failed || 0, node.total)) / node.total : 1,
[Fields.arc + 'failed']: node.total ? Math.min(node.failed || 0, node.total) / node.total : 0, [Fields.arc + 'failed']: node.total ? Math.min(node.failed || 0, node.total) / node.total : 0,
[Fields.isInstrumented]: node.isInstrumented ?? true,
}); });
} }
for (const edgeId of Object.keys(edgesMap)) { for (const edgeId of Object.keys(edgesMap)) {

View File

@ -17,6 +17,7 @@ export type NodeDatum = SimulationNodeDatum & {
icon?: IconName; icon?: IconName;
nodeRadius?: Field; nodeRadius?: Field;
highlighted: boolean; highlighted: boolean;
isInstrumented?: boolean;
}; };
export type NodeDatumFromEdge = NodeDatum & { mainStatNumeric?: number; secondaryStatNumeric?: number }; export type NodeDatumFromEdge = NodeDatum & { mainStatNumeric?: number; secondaryStatNumeric?: number };

View File

@ -19,7 +19,7 @@ describe('processNodes', () => {
it('returns proper nodes and edges', async () => { it('returns proper nodes and edges', async () => {
const { nodes, edges, legend } = processNodes( const { nodes, edges, legend } = processNodes(
makeNodesDataFrame(3), makeNodesDataFrame(3, [{ isinstrumented: false }]),
makeEdgesDataFrame([ makeEdgesDataFrame([
{ source: '0', target: '1' }, { source: '0', target: '1' },
{ source: '0', target: '2' }, { source: '0', target: '2' },
@ -28,7 +28,7 @@ describe('processNodes', () => {
); );
expect(nodes).toEqual([ expect(nodes).toEqual([
makeNodeDatum(), makeNodeDatum({ isInstrumented: false }),
makeNodeDatum({ dataFrameRowIndex: 1, id: '1', incoming: 1, title: 'service:1' }), makeNodeDatum({ dataFrameRowIndex: 1, id: '1', incoming: 1, title: 'service:1' }),
makeNodeDatum({ dataFrameRowIndex: 2, id: '2', incoming: 2, title: 'service:2' }), makeNodeDatum({ dataFrameRowIndex: 2, id: '2', incoming: 2, title: 'service:2' }),
]); ]);
@ -366,6 +366,7 @@ function makeNodeDatum(options: Partial<NodeDatum> = {}) {
type: 'number', type: 'number',
values: [40, 40, 40], values: [40, 40, 40],
}, },
isInstrumented: true,
...options, ...options,
}; };
} }

View File

@ -58,6 +58,7 @@ export type NodeFields = {
icon?: Field; icon?: Field;
nodeRadius?: Field; nodeRadius?: Field;
highlighted?: Field; highlighted?: Field;
isInstrumented?: Field;
}; };
export function getNodeFields(nodes: DataFrame): NodeFields { export function getNodeFields(nodes: DataFrame): NodeFields {
@ -80,6 +81,7 @@ export function getNodeFields(nodes: DataFrame): NodeFields {
highlighted: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.highlighted.toLowerCase()), highlighted: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.highlighted.toLowerCase()),
fixedX: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedX.toLowerCase()), fixedX: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedX.toLowerCase()),
fixedY: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedY.toLowerCase()), fixedY: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.fixedY.toLowerCase()),
isInstrumented: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.isInstrumented.toLowerCase()),
}; };
} }
@ -364,6 +366,7 @@ function makeNodeDatum(id: string, nodeFields: NodeFields, index: number): NodeD
highlighted: nodeFields.highlighted?.values[index] || false, highlighted: nodeFields.highlighted?.values[index] || false,
x: nodeFields.fixedX?.values[index] ?? undefined, x: nodeFields.fixedX?.values[index] ?? undefined,
y: nodeFields.fixedY?.values[index] ?? undefined, y: nodeFields.fixedY?.values[index] ?? undefined,
isInstrumented: nodeFields.isInstrumented?.values[index] ?? true,
}; };
} }
@ -384,16 +387,19 @@ export function statToString(config: FieldConfig, value: number | string): strin
* Utilities mainly for testing * Utilities mainly for testing
*/ */
export function makeNodesDataFrame(count: number) { export function makeNodesDataFrame(
count: number,
partialNodes: Array<Partial<Record<NodeGraphDataFrameFieldNames, unknown>>> = []
) {
const frame = nodesFrame(); const frame = nodesFrame();
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
frame.add(makeNode(i)); frame.add(makeNode(i, partialNodes[i]));
} }
return frame; return frame;
} }
function makeNode(index: number) { function makeNode(index: number, partialNode: Partial<Record<NodeGraphDataFrameFieldNames, unknown>> = {}) {
return { return {
id: index.toString(), id: index.toString(),
title: `service:${index}`, title: `service:${index}`,
@ -405,6 +411,8 @@ function makeNode(index: number) {
color: 0.5, color: 0.5,
icon: 'database', icon: 'database',
noderadius: 40, noderadius: 40,
isinstrumented: true,
...partialNode,
}; };
} }
@ -453,6 +461,10 @@ function nodesFrame() {
values: [], values: [],
type: FieldType.number, type: FieldType.number,
}, },
[NodeGraphDataFrameFieldNames.isInstrumented]: {
values: [],
type: FieldType.boolean,
},
}; };
return new MutableDataFrame({ return new MutableDataFrame({