Jaeger: remove unused code (#108612)

* remove isSearchFormValid

* remove getTimeRange

* remove convertTagsLogfmt

* remove createTableFrame

* remove transformToLogfmt

* remove types

* remove mapJaegerDependenciesResponse

* trigger workflow
This commit is contained in:
Gareth 2025-07-31 11:30:15 +01:00 committed by GitHub
parent a3e1920983
commit 04a563cb09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 4 additions and 1018 deletions

View File

@ -3498,12 +3498,6 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/influxdb/response_parser.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"]
],
"public/app/plugins/datasource/jaeger/datasource.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

View File

@ -1,3 +0,0 @@
This directory contains dependencies that we duplicated from Grafana core while working on the decoupling of Jaeger from such core.
The long-term goal is to move these files away from here by replacing them with packages.
As such, they are only temporary and meant to be used internally to this package, please avoid using them for example as dependencies (imports) in other data source plugins.

View File

@ -1,57 +0,0 @@
// Copyright (c) 2020 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { memoize } from 'lodash';
import { TraceSpan } from '../types';
function _getTraceNameImpl(spans: TraceSpan[]) {
// Use a span with no references to another span in given array
// prefering the span with the fewest references
// using start time as a tie breaker
let candidateSpan: TraceSpan | undefined;
const allIDs: Set<string> = new Set(spans.map(({ spanID }) => spanID));
for (let i = 0; i < spans.length; i++) {
const hasInternalRef =
spans[i].references &&
spans[i].references.some(({ traceID, spanID }) => traceID === spans[i].traceID && allIDs.has(spanID));
if (hasInternalRef) {
continue;
}
if (!candidateSpan) {
candidateSpan = spans[i];
continue;
}
const thisRefLength = (spans[i].references && spans[i].references.length) || 0;
const candidateRefLength = (candidateSpan.references && candidateSpan.references.length) || 0;
if (
thisRefLength < candidateRefLength ||
(thisRefLength === candidateRefLength && spans[i].startTime < candidateSpan.startTime)
) {
candidateSpan = spans[i];
}
}
return candidateSpan ? `${candidateSpan.process.serviceName}: ${candidateSpan.operationName}` : '';
}
export const getTraceName = memoize(_getTraceNameImpl, (spans: TraceSpan[]) => {
if (!spans.length) {
return 0;
}
return spans[0].traceID;
});

View File

@ -1,201 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { isEqual as _isEqual } from 'lodash';
import { TraceKeyValuePair } from '@grafana/data';
import { getTraceSpanIdsAsTree } from '../selectors/trace';
import { TraceSpan, Trace, TraceResponse, TraceProcess } from '../types';
import TreeNode from '../utils/TreeNode';
import { getConfigValue } from '../utils/config/get-config';
import { getTraceName } from './trace-viewer';
function deduplicateTags(tags: TraceKeyValuePair[]) {
const warningsHash: Map<string, string> = new Map<string, string>();
const dedupedTags: TraceKeyValuePair[] = tags.reduce<TraceKeyValuePair[]>((uniqueTags, tag) => {
if (!uniqueTags.some((t) => t.key === tag.key && t.value === tag.value)) {
uniqueTags.push(tag);
} else {
warningsHash.set(`${tag.key}:${tag.value}`, `Duplicate tag "${tag.key}:${tag.value}"`);
}
return uniqueTags;
}, []);
const warnings = Array.from(warningsHash.values());
return { dedupedTags, warnings };
}
function orderTags(tags: TraceKeyValuePair[], topPrefixes?: string[]) {
const orderedTags: TraceKeyValuePair[] = tags?.slice() ?? [];
const tp = (topPrefixes || []).map((p: string) => p.toLowerCase());
orderedTags.sort((a, b) => {
const aKey = a.key.toLowerCase();
const bKey = b.key.toLowerCase();
for (let i = 0; i < tp.length; i++) {
const p = tp[i];
if (aKey.startsWith(p) && !bKey.startsWith(p)) {
return -1;
}
if (!aKey.startsWith(p) && bKey.startsWith(p)) {
return 1;
}
}
if (aKey > bKey) {
return 1;
}
if (aKey < bKey) {
return -1;
}
return 0;
});
return orderedTags;
}
/**
* NOTE: Mutates `data` - Transform the HTTP response data into the form the app
* generally requires.
*/
export default function transformTraceData(data: TraceResponse | undefined): Trace | null {
if (!data?.traceID) {
return null;
}
const traceID = data.traceID.toLowerCase();
let traceEndTime = 0;
let traceStartTime = Number.MAX_SAFE_INTEGER;
const spanIdCounts = new Map();
const spanMap = new Map<string, TraceSpan>();
// filter out spans with empty start times
// eslint-disable-next-line no-param-reassign
data.spans = data.spans.filter((span) => Boolean(span.startTime));
// Sort process tags
data.processes = Object.entries(data.processes).reduce<Record<string, TraceProcess>>((processes, [id, process]) => {
processes[id] = {
...process,
tags: orderTags(process.tags),
};
return processes;
}, {});
const max = data.spans.length;
for (let i = 0; i < max; i++) {
const span: TraceSpan = data.spans[i] as TraceSpan;
const { startTime, duration, processID } = span;
let spanID = span.spanID;
// check for start / end time for the trace
if (startTime < traceStartTime) {
traceStartTime = startTime;
}
if (startTime + duration > traceEndTime) {
traceEndTime = startTime + duration;
}
// make sure span IDs are unique
const idCount = spanIdCounts.get(spanID);
if (idCount != null) {
// eslint-disable-next-line no-console
console.warn(`Dupe spanID, ${idCount + 1} x ${spanID}`, span, spanMap.get(spanID));
if (_isEqual(span, spanMap.get(spanID))) {
// eslint-disable-next-line no-console
console.warn('\t two spans with same ID have `isEqual(...) === true`');
}
spanIdCounts.set(spanID, idCount + 1);
spanID = `${spanID}_${idCount}`;
span.spanID = spanID;
} else {
spanIdCounts.set(spanID, 1);
}
span.process = data.processes[processID];
spanMap.set(spanID, span);
}
// tree is necessary to sort the spans, so children follow parents, and
// siblings are sorted by start time
const tree = getTraceSpanIdsAsTree(data, spanMap);
const spans: TraceSpan[] = [];
const svcCounts: Record<string, number> = {};
tree.walk((spanID: string, node: TreeNode<string>, depth = 0) => {
if (spanID === '__root__') {
return;
}
if (typeof spanID !== 'string') {
return;
}
const span = spanMap.get(spanID);
if (!span) {
return;
}
const { serviceName } = span.process;
svcCounts[serviceName] = (svcCounts[serviceName] || 0) + 1;
span.relativeStartTime = span.startTime - traceStartTime;
span.depth = depth - 1;
span.hasChildren = node.children.length > 0;
span.childSpanCount = node.children.length;
span.warnings = span.warnings || [];
span.tags = span.tags || [];
span.references = span.references || [];
span.childSpanIds = node.children
.slice()
.sort((a, b) => {
const spanA = spanMap.get(a.value)!;
const spanB = spanMap.get(b.value)!;
return spanB.startTime + spanB.duration - (spanA.startTime + spanA.duration);
})
.map((each) => each.value);
const tagsInfo = deduplicateTags(span.tags);
span.tags = orderTags(tagsInfo.dedupedTags, getConfigValue('topTagPrefixes'));
span.warnings = span.warnings.concat(tagsInfo.warnings);
span.references.forEach((ref, index) => {
const refSpan = spanMap.get(ref.spanID);
if (refSpan) {
// eslint-disable-next-line no-param-reassign
ref.span = refSpan;
if (index > 0) {
// Don't take into account the parent, just other references.
refSpan.subsidiarilyReferencedBy = refSpan.subsidiarilyReferencedBy || [];
refSpan.subsidiarilyReferencedBy.push({
spanID,
traceID,
span,
refType: ref.refType,
});
}
}
});
spans.push(span);
});
const traceName = getTraceName(spans);
const services = Object.keys(svcCounts).map((name) => ({ name, numberOfSpans: svcCounts[name] }));
return {
services,
spans,
traceID,
traceName,
// can't use spread operator for intersection types
// repl: https://goo.gl/4Z23MJ
// issue: https://github.com/facebook/flow/issues/1511
processes: data.processes,
duration: traceEndTime - traceStartTime,
startTime: traceStartTime,
endTime: traceEndTime,
};
}

View File

@ -1,65 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceResponse, TraceSpanData } from '../types/trace';
import TreeNode from '../utils/TreeNode';
const TREE_ROOT_ID = '__root__';
/**
* Build a tree of { value: spanID, children } items derived from the
* `span.references` information. The tree represents the grouping of parent /
* child relationships. The root-most node is nominal in that
* `.value === TREE_ROOT_ID`. This is done because a root span (the main trace
* span) is not always included with the trace data. Thus, there can be
* multiple top-level spans, and the root node acts as their common parent.
*
* The children are sorted by `span.startTime` after the tree is built.
*
* @param {Trace} trace The trace to build the tree of spanIDs.
* @return {TreeNode} A tree of spanIDs derived from the relationships
* between spans in the trace.
*/
export function getTraceSpanIdsAsTree(trace: TraceResponse, spanMap: Map<string, TraceSpanData> | null = null) {
const nodesById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, new TreeNode(span.spanID)]));
const spansById = spanMap ?? new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span]));
const root = new TreeNode(TREE_ROOT_ID);
trace.spans.forEach((span: TraceSpanData) => {
const node = nodesById.get(span.spanID)!;
if (Array.isArray(span.references) && span.references.length) {
const { refType, spanID: parentID } = span.references[0];
if (refType === 'CHILD_OF' || refType === 'FOLLOWS_FROM') {
const parent = nodesById.get(parentID) || root;
parent.children?.push(node);
} else {
throw new Error(`Unrecognized ref type: ${refType}`);
}
} else {
root.children.push(node);
}
});
const comparator = (nodeA: TreeNode<string>, nodeB: TreeNode<string>) => {
const a: TraceSpanData | undefined = nodeA?.value ? spansById.get(nodeA.value.toString()) : undefined;
const b: TraceSpanData | undefined = nodeB?.value ? spansById.get(nodeB.value.toString()) : undefined;
return +(a?.startTime! > b?.startTime!) || +(a?.startTime === b?.startTime) - 1;
};
trace.spans.forEach((span: TraceSpanData) => {
const node = nodesById.get(span.spanID);
if (node!.children.length > 1) {
node?.children.sort(comparator);
}
});
root.children.sort(comparator);
return root;
}

View File

@ -1,15 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export type { TraceSpan, TraceResponse, Trace, TraceProcess, TraceLink, CriticalPathSection } from './trace';

View File

@ -1,102 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceKeyValuePair, TraceLog } from '@grafana/data';
/**
* All timestamps are in microseconds
*/
export type TraceLink = {
url: string;
text: string;
};
export type TraceProcess = {
serviceName: string;
tags: TraceKeyValuePair[];
};
export type TraceSpanReference = {
refType: 'CHILD_OF' | 'FOLLOWS_FROM';
// eslint-disable-next-line no-use-before-define
span?: TraceSpan | null | undefined;
spanID: string;
traceID: string;
tags?: TraceKeyValuePair[];
};
export type TraceSpanData = {
spanID: string;
traceID: string;
processID: string;
operationName: string;
// Times are in microseconds
startTime: number;
duration: number;
logs: TraceLog[];
tags?: TraceKeyValuePair[];
kind?: string;
statusCode?: number;
statusMessage?: string;
instrumentationLibraryName?: string;
instrumentationLibraryVersion?: string;
traceState?: string;
references?: TraceSpanReference[];
warnings?: string[] | null;
stackTraces?: string[];
flags: number;
errorIconColor?: string;
dataFrameRowIndex?: number;
childSpanIds?: string[];
};
export type TraceSpan = TraceSpanData & {
depth: number;
hasChildren: boolean;
childSpanCount: number;
process: TraceProcess;
relativeStartTime: number;
tags: NonNullable<TraceSpanData['tags']>;
references: NonNullable<TraceSpanData['references']>;
warnings: NonNullable<TraceSpanData['warnings']>;
childSpanIds: NonNullable<TraceSpanData['childSpanIds']>;
subsidiarilyReferencedBy: TraceSpanReference[];
};
export type TraceData = {
processes: Record<string, TraceProcess>;
traceID: string;
warnings?: string[] | null;
};
export type TraceResponse = TraceData & {
spans: TraceSpanData[];
};
export type Trace = TraceData & {
duration: number;
endTime: number;
spans: TraceSpan[];
startTime: number;
traceName: string;
services: Array<{ name: string; numberOfSpans: number }>;
};
// It is a section of span that lies on critical path
export type CriticalPathSection = {
spanId: string;
section_start: number;
section_end: number;
};

View File

@ -1,139 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
export default class TreeNode<TValue> {
value: TValue;
children: Array<TreeNode<TValue>>;
static iterFunction<TValue>(
fn: ((value: TValue, node: TreeNode<TValue>, depth: number) => TreeNode<TValue> | null) | Function,
depth = 0
) {
return (node: TreeNode<TValue>) => fn(node.value, node, depth);
}
static searchFunction<TValue>(search: Function | TreeNode<TValue>) {
if (typeof search === 'function') {
return search;
}
return (value: TValue, node: TreeNode<TValue>) => (search instanceof TreeNode ? node === search : value === search);
}
constructor(value: TValue, children: Array<TreeNode<TValue>> = []) {
this.value = value;
this.children = children;
}
get depth(): number {
return this.children.reduce((depth, child) => Math.max(child.depth + 1, depth), 1);
}
get size() {
let i = 0;
this.walk(() => i++);
return i;
}
addChild(child: TreeNode<TValue> | TValue) {
this.children.push(child instanceof TreeNode ? child : new TreeNode(child));
return this;
}
find(search: Function | TreeNode<TValue>): TreeNode<TValue> | null {
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
if (searchFn(this)) {
return this;
}
for (let i = 0; i < this.children.length; i++) {
const result = this.children[i].find(search);
if (result) {
return result;
}
}
return null;
}
getPath(search: Function | TreeNode<TValue>) {
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
const findPath = (
currentNode: TreeNode<TValue>,
currentPath: Array<TreeNode<TValue>>
): Array<TreeNode<TValue>> | null => {
// skip if we already found the result
const attempt = currentPath.concat([currentNode]);
// base case: return the array when there is a match
if (searchFn(currentNode)) {
return attempt;
}
for (let i = 0; i < currentNode.children.length; i++) {
const child = currentNode.children[i];
const match = findPath(child, attempt);
if (match) {
return match;
}
}
return null;
};
return findPath(this, []);
}
walk(fn: (spanID: TValue, node: TreeNode<TValue>, depth: number) => void, startDepth = 0) {
type StackEntry = {
node: TreeNode<TValue>;
depth: number;
};
const nodeStack: StackEntry[] = [];
let actualDepth = startDepth;
nodeStack.push({ node: this, depth: actualDepth });
while (nodeStack.length) {
const entry: StackEntry = nodeStack[nodeStack.length - 1];
nodeStack.pop();
const { node, depth } = entry;
fn(node.value, node, depth);
actualDepth = depth + 1;
let i = node.children.length - 1;
while (i >= 0) {
nodeStack.push({ node: node.children[i], depth: actualDepth });
i--;
}
}
}
paths(fn: (pathIds: TValue[]) => void) {
type StackEntry = {
node: TreeNode<TValue>;
childIndex: number;
};
const stack: StackEntry[] = [];
stack.push({ node: this, childIndex: 0 });
const paths: TValue[] = [];
while (stack.length) {
const { node, childIndex } = stack[stack.length - 1];
if (node.children.length >= childIndex + 1) {
stack[stack.length - 1].childIndex++;
stack.push({ node: node.children[childIndex], childIndex: 0 });
} else {
if (node.children.length === 0) {
const path = stack.map((item) => item.node.value);
fn(path);
}
stack.pop();
}
}
return paths;
}
}

View File

@ -1,40 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const FALLBACK_DAG_MAX_NUM_SERVICES = 100;
export default Object.defineProperty(
{
archiveEnabled: false,
dependencies: {
dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES,
menuEnabled: true,
},
linkPatterns: [],
search: {
maxLookback: {
label: '2 Days',
value: '2d',
},
maxLimit: 1500,
},
tracking: {
gaID: null,
trackErrors: true,
},
},
// fields that should be individually merged vs wholesale replaced
'__mergeFields',
{ value: ['dependencies', 'search', 'tracking'] }
);

View File

@ -1,29 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { get as _get } from 'lodash';
import defaultConfig from './default-config';
/**
* Merge the embedded config from the query service (if present) with the
* default config from `../../constants/default-config`.
*/
export default function getConfig() {
return defaultConfig;
}
export function getConfigValue(path: string) {
return _get(getConfig(), path);
}

View File

@ -8,7 +8,6 @@ import { fuzzyMatch, InlineField, InlineFieldRow, Input, Select } from '@grafana
import { JaegerDatasource } from '../datasource';
import { JaegerQuery } from '../types';
import { transformToLogfmt } from '../util';
const durationPlaceholder = 'e.g. 1.2s, 100ms, 500us';
@ -150,7 +149,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in logfmt.">
<Input
id="tags"
value={transformToLogfmt(query.tags)}
value={query.tags}
placeholder="http.status_code=200 error=true"
onChange={(v) =>
onChange({

View File

@ -136,16 +136,6 @@ describe('node graph functionality', () => {
});
});
describe('time range', () => {
it('should calculate correct time range', async () => {
const ds = new JaegerDatasource(defaultSettings);
const timeRange = ds.getTimeRange();
const now = Date.now();
expect(timeRange.end).toBeCloseTo(now * 1000, -4);
expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, -4);
});
});
function setupQueryMock(type: 'trace' | 'search') {
return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => {
if (type === 'search') {

View File

@ -6,10 +6,7 @@ import {
DataQueryResponse,
DataSourceInstanceSettings,
DataSourceJsonData,
dateMath,
DateTime,
FieldType,
getDefaultTimeRange,
MutableDataFrame,
ScopedVars,
toDataFrame,
@ -45,10 +42,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
return await this.getResource(url, params);
}
isSearchFormValid(query: JaegerQuery): boolean {
return !!query.service;
}
query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> {
// At this moment we expect only one target. In case we somehow change the UI to be able to show multiple
// traces at one we need to change this.
@ -122,25 +115,11 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
return await super.testDatasource();
}
getTimeRange(range = getDefaultTimeRange()): { start: number; end: number } {
return {
start: getTime(range.from, false),
end: getTime(range.to, true),
};
}
getQueryDisplayText(query: JaegerQuery) {
return query.query || '';
}
}
function getTime(date: string | DateTime, roundUp: boolean) {
if (typeof date === 'string') {
date = dateMath.parse(date, roundUp)!;
}
return date.valueOf() * 1000;
}
const emptyTraceDataFrame = new MutableDataFrame({
fields: [
{

View File

@ -1,106 +0,0 @@
import { mapJaegerDependenciesResponse } from './dependencyGraphTransform';
describe('dependencyGraphTransform', () => {
it('should transform Jaeger dependencies API response', () => {
const data = {
data: [
{
parent: 'serviceA',
child: 'serviceB',
callCount: 1,
},
{
parent: 'serviceA',
child: 'serviceC',
callCount: 2,
},
{
parent: 'serviceB',
child: 'serviceC',
callCount: 3,
},
],
total: 0,
limit: 0,
offset: 0,
};
const res = mapJaegerDependenciesResponse({ data });
expect(res).toMatchObject({
data: [
{
fields: [
{
config: {},
name: 'id',
type: 'string',
values: ['serviceA', 'serviceB', 'serviceC'],
},
{
config: {},
name: 'title',
type: 'string',
values: ['serviceA', 'serviceB', 'serviceC'],
},
],
meta: { preferredVisualisationType: 'nodeGraph' },
},
{
fields: [
{
config: {},
name: 'id',
type: 'string',
values: ['serviceA--serviceB', 'serviceA--serviceC', 'serviceB--serviceC'],
},
{
config: {},
name: 'target',
type: 'string',
values: ['serviceB', 'serviceC', 'serviceC'],
},
{
config: {},
name: 'source',
type: 'string',
values: ['serviceA', 'serviceA', 'serviceB'],
},
{
config: { displayName: 'Call count' },
name: 'mainstat',
type: 'string',
values: [1, 2, 3],
},
],
meta: { preferredVisualisationType: 'nodeGraph' },
},
],
});
});
it('should transform Jaeger API error', () => {
const data = {
total: 0,
limit: 0,
offset: 0,
errors: [
{
code: 400,
msg: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax',
},
],
};
const res = mapJaegerDependenciesResponse({ data });
expect(res).toEqual({
data: [],
errors: [
{
message: 'unable to parse param \'endTs\': strconv.ParseInt: parsing "foo": invalid syntax',
status: 400,
},
],
});
});
});

View File

@ -1,125 +0,0 @@
import {
DataFrame,
DataQueryResponse,
FieldType,
MutableDataFrame,
NodeGraphDataFrameFieldNames as Fields,
} from '@grafana/data';
import { JaegerServiceDependency } from './types';
interface Node {
[Fields.id]: string;
[Fields.title]: string;
}
interface Edge {
[Fields.id]: string;
[Fields.target]: string;
[Fields.source]: string;
[Fields.mainStat]: number;
}
/**
* Error schema used by the Jaeger dependencies API.
*/
interface JaegerDependenciesResponseError {
code: number;
msg: string;
}
interface JaegerDependenciesResponse {
data?: {
errors?: JaegerDependenciesResponseError[];
data?: JaegerServiceDependency[];
};
}
/**
* Transforms a Jaeger dependencies API response to a Grafana {@link DataQueryResponse}.
* @param response Raw response data from the API proxy.
*/
export function mapJaegerDependenciesResponse(response: JaegerDependenciesResponse): DataQueryResponse {
const errors = response?.data?.errors;
if (errors) {
return {
data: [],
errors: errors.map((e: JaegerDependenciesResponseError) => ({ message: e.msg, status: e.code })),
};
}
const dependencies = response?.data?.data;
if (dependencies) {
return {
data: convertDependenciesToGraph(dependencies),
};
}
return { data: [] };
}
/**
* Converts a list of Jaeger service dependencies to a Grafana {@link DataFrame} array suitable for the node graph panel.
* @param dependencies List of Jaeger service dependencies as returned by the Jaeger dependencies API.
*/
function convertDependenciesToGraph(dependencies: JaegerServiceDependency[]): DataFrame[] {
const servicesByName = new Map<string, Node>();
const edges: Edge[] = [];
for (const dependency of dependencies) {
addServiceNode(dependency.parent, servicesByName);
addServiceNode(dependency.child, servicesByName);
edges.push({
[Fields.id]: dependency.parent + '--' + dependency.child,
[Fields.target]: dependency.child,
[Fields.source]: dependency.parent,
[Fields.mainStat]: dependency.callCount,
});
}
const nodesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.title, type: FieldType.string },
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
const edgesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.target, type: FieldType.string },
{ name: Fields.source, type: FieldType.string },
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Call count' } },
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
for (const node of servicesByName.values()) {
nodesFrame.add(node);
}
for (const edge of edges) {
edgesFrame.add(edge);
}
return [nodesFrame, edgesFrame];
}
/**
* Convenience function to register a service node in the dependency graph.
* @param service Name of the service to register.
* @param servicesByName Map of service nodes keyed name.
*/
function addServiceNode(service: string, servicesByName: Map<string, Node>) {
if (!servicesByName.has(service)) {
servicesByName.set(service, {
[Fields.id]: service,
[Fields.title]: service,
});
}
}

View File

@ -1,13 +1,5 @@
import {
DataFrame,
DataSourceInstanceSettings,
FieldType,
MutableDataFrame,
TraceLog,
TraceSpanRow,
} from '@grafana/data';
import { DataFrame, FieldType, MutableDataFrame, TraceLog, TraceSpanRow } from '@grafana/data';
import transformTraceData from './_importedDependencies/model/transform-trace-data';
import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types';
export function createTraceFrame(data: TraceResponse): DataFrame {
@ -68,62 +60,6 @@ function toSpanRow(span: Span, processes: Record<string, TraceProcess>): TraceSp
};
}
export function createTableFrame(data: TraceResponse[], instanceSettings: DataSourceInstanceSettings): DataFrame {
const frame = new MutableDataFrame({
fields: [
{
name: 'traceID',
type: FieldType.string,
config: {
unit: 'string',
displayNameFromDS: 'Trace ID',
links: [
{
title: 'Trace: ${__value.raw}',
url: '',
internal: {
datasourceUid: instanceSettings.uid,
datasourceName: instanceSettings.name,
query: {
query: '${__value.raw}',
},
},
},
],
},
},
{ name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Trace name' } },
{ name: 'startTime', type: FieldType.time, config: { displayNameFromDS: 'Start time' } },
{ name: 'duration', type: FieldType.number, config: { displayNameFromDS: 'Duration', unit: 'µs' } },
],
meta: {
preferredVisualisationType: 'table',
},
});
// Show the most recent traces
const traceData = data.map(transformToTraceData).sort((a, b) => b?.startTime! - a?.startTime!);
for (const trace of traceData) {
frame.add(trace);
}
return frame;
}
function transformToTraceData(data: TraceResponse) {
const traceData = transformTraceData(data);
if (!traceData) {
return;
}
return {
traceID: traceData.traceID,
startTime: traceData.startTime / 1000,
duration: traceData.duration,
traceName: traceData.traceName,
};
}
export function transformToJaeger(data: MutableDataFrame): JaegerResponse {
let traceResponse: TraceResponse = {
traceID: '',

View File

@ -1,9 +1,5 @@
import { DataQuery, TraceKeyValuePair, TraceLog } from '@grafana/data';
export type TraceLink = {
url: string;
text: string;
};
import { TraceKeyValuePair, TraceLog } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
export type TraceProcess = {
serviceName: string;

View File

@ -1,26 +0,0 @@
import logfmt from 'logfmt';
export function convertTagsLogfmt(tags: string | undefined) {
if (!tags) {
return '';
}
const data = logfmt.parse(tags);
Object.keys(data).forEach((key) => {
const value = data[key];
if (typeof value !== 'string') {
data[key] = String(value);
}
});
return JSON.stringify(data);
}
export function transformToLogfmt(tags: string | undefined) {
if (!tags) {
return '';
}
try {
return logfmt.stringify(JSON.parse(tags));
} catch {
return tags;
}
}