mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Jaeger: Decouple Jaeger plugin (#81377)
This commit is contained in:
parent
83597e6c37
commit
d1f791cf1f
@ -5008,6 +5008,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"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/trace.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
@ -5019,6 +5025,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/jaeger/testResponse.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
|
14
.eslintrc
14
.eslintrc
@ -96,16 +96,22 @@
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"public/app/plugins/datasource/azuremonitor/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/azuremonitor/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/cloud-monitoring/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/cloud-monitoring/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/elasticsearch/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-postgresql-datasource/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-postgresql-datasource/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-pyroscope-datasource/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-pyroscope-datasource/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-testdata-datasource/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/grafana-testdata-datasource/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/azuremonitor/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/azuremonitor/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/cloud-monitoring/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/cloud-monitoring/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/jaeger/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/jaeger/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/loki/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/loki/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/mysql/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/mysql/**/*.{ts,tsx}",
|
||||
"public/app/plugins/datasource/parca/*.{ts,tsx}",
|
||||
|
@ -11,6 +11,7 @@ on:
|
||||
- grafana-azure-monitor-datasource
|
||||
- grafana-pyroscope-datasource
|
||||
- grafana-testdata-datasource
|
||||
- jaeger
|
||||
- parca
|
||||
- stackdriver
|
||||
- tempo
|
||||
|
@ -961,7 +961,7 @@
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
},
|
||||
|
@ -13,8 +13,6 @@ const grafanaPlugin = async () =>
|
||||
const influxdbPlugin = async () =>
|
||||
await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module');
|
||||
const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module');
|
||||
const jaegerPlugin = async () =>
|
||||
await import(/* webpackChunkName: "jaegerPlugin" */ 'app/plugins/datasource/jaeger/module');
|
||||
const mixedPlugin = async () =>
|
||||
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
|
||||
const mysqlPlugin = async () =>
|
||||
@ -77,7 +75,6 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
|
||||
'core:plugin/grafana': grafanaPlugin,
|
||||
'core:plugin/influxdb': influxdbPlugin,
|
||||
'core:plugin/loki': lokiPlugin,
|
||||
'core:plugin/jaeger': jaegerPlugin,
|
||||
'core:plugin/mixed': mixedPlugin,
|
||||
'core:plugin/mysql': mysqlPlugin,
|
||||
'core:plugin/grafana-postgresql-datasource': postgresPlugin,
|
||||
|
1
public/app/plugins/datasource/jaeger/CHANGELOG.md
Normal file
1
public/app/plugins/datasource/jaeger/CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
||||
# Changelog
|
@ -1,7 +1,3 @@
|
||||
# Jaeger Data Source - Native Plugin
|
||||
# Grafana Jaeger Data Source - Native Plugin
|
||||
|
||||
Grafana ships with **built in** support for Jaeger, an open source, end-to-end distributed tracing system.
|
||||
|
||||
Read more about it here:
|
||||
|
||||
[https://docs.grafana.org/datasources/jaeger/](https://docs.grafana.org/datasources/jaeger/)
|
||||
[https://docs.grafana.org/datasources/jaeger/](Grafana plugin for the Jaeger data source).
|
||||
|
@ -0,0 +1,3 @@
|
||||
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.
|
@ -0,0 +1,57 @@
|
||||
// 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;
|
||||
});
|
@ -0,0 +1,199 @@
|
||||
// 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 { getTraceSpanIdsAsTree } from '../selectors/trace';
|
||||
import { TraceKeyValuePair, 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,
|
||||
};
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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 {
|
||||
TraceSpan,
|
||||
TraceResponse,
|
||||
Trace,
|
||||
TraceProcess,
|
||||
TraceKeyValuePair,
|
||||
TraceLink,
|
||||
CriticalPathSection,
|
||||
} from './trace';
|
@ -0,0 +1,112 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* All timestamps are in microseconds
|
||||
*/
|
||||
|
||||
// TODO: Everett Tech Debt: Fix KeyValuePair types
|
||||
export type TraceKeyValuePair = {
|
||||
key: string;
|
||||
type?: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export type TraceLink = {
|
||||
url: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type TraceLog = {
|
||||
timestamp: number;
|
||||
fields: TraceKeyValuePair[];
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
@ -0,0 +1,139 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
// 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'] }
|
||||
);
|
@ -0,0 +1,29 @@
|
||||
// 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);
|
||||
}
|
@ -2,17 +2,19 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
|
||||
import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { BackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { JaegerDatasource, JaegerJsonData } from '../datasource';
|
||||
import { createFetchResponse } from '../helpers/createFetchResponse';
|
||||
import { testResponse } from '../testResponse';
|
||||
import { JaegerQuery } from '../types';
|
||||
|
||||
import SearchForm from './SearchForm';
|
||||
|
||||
export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getTemplateSrv: () => ({
|
||||
|
@ -2,11 +2,9 @@ import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { fuzzyMatch, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { JaegerDatasource } from '../datasource';
|
||||
import { JaegerQuery } from '../types';
|
||||
@ -27,6 +25,7 @@ const allOperationsOption: SelectableValue<string> = {
|
||||
};
|
||||
|
||||
export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
const [alertText, setAlertText] = useState('');
|
||||
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [operationOptions, setOperationOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [isLoading, setIsLoading] = useState<{
|
||||
@ -53,10 +52,11 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
}));
|
||||
|
||||
const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
|
||||
setAlertText('');
|
||||
return filteredOptions;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
setAlertText(`Error: ${error.message}`);
|
||||
}
|
||||
return [];
|
||||
} finally {
|
||||
@ -94,121 +94,124 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
}, [datasource, query.service, loadOptions, query.operation]);
|
||||
|
||||
return (
|
||||
<div className={css({ maxWidth: '500px' })}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Service Name" labelWidth={14} grow>
|
||||
<Select
|
||||
inputId="service"
|
||||
options={serviceOptions}
|
||||
onOpenMenu={() => loadOptions('/api/services', 'services')}
|
||||
isLoading={isLoading.services}
|
||||
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
|
||||
placeholder="Select a service"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
service: v?.value!,
|
||||
operation: query.service !== v?.value ? undefined : query.operation,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-service-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Operation Name" labelWidth={14} grow disabled={!query.service}>
|
||||
<Select
|
||||
inputId="operation"
|
||||
options={operationOptions}
|
||||
onOpenMenu={() =>
|
||||
loadOptions(
|
||||
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
|
||||
'operations'
|
||||
)
|
||||
}
|
||||
isLoading={isLoading.operations}
|
||||
value={operationOptions?.find((v) => v.value === query.operation) || null}
|
||||
placeholder="Select an operation"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
operation: v?.value! || undefined,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-operation-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in logfmt.">
|
||||
<Input
|
||||
id="tags"
|
||||
value={transformToLogfmt(query.tags)}
|
||||
placeholder="http.status_code=200 error=true"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
tags: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Min Duration" labelWidth={14} grow>
|
||||
<Input
|
||||
id="minDuration"
|
||||
name="minDuration"
|
||||
value={query.minDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
minDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Max Duration" labelWidth={14} grow>
|
||||
<Input
|
||||
id="maxDuration"
|
||||
name="maxDuration"
|
||||
value={query.maxDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
maxDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Limit" labelWidth={14} grow tooltip="Maximum number of returned results">
|
||||
<Input
|
||||
id="limit"
|
||||
name="limit"
|
||||
value={query.limit || ''}
|
||||
type="number"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
<>
|
||||
<div className={css({ maxWidth: '500px' })}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Service Name" labelWidth={14} grow>
|
||||
<Select
|
||||
inputId="service"
|
||||
options={serviceOptions}
|
||||
onOpenMenu={() => loadOptions('/api/services', 'services')}
|
||||
isLoading={isLoading.services}
|
||||
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
|
||||
placeholder="Select a service"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
service: v?.value!,
|
||||
operation: query.service !== v?.value ? undefined : query.operation,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-service-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Operation Name" labelWidth={14} grow disabled={!query.service}>
|
||||
<Select
|
||||
inputId="operation"
|
||||
options={operationOptions}
|
||||
onOpenMenu={() =>
|
||||
loadOptions(
|
||||
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
|
||||
'operations'
|
||||
)
|
||||
}
|
||||
isLoading={isLoading.operations}
|
||||
value={operationOptions?.find((v) => v.value === query.operation) || null}
|
||||
placeholder="Select an operation"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
operation: v?.value! || undefined,
|
||||
})
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
isClearable
|
||||
aria-label={'select-operation-name'}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Tags" labelWidth={14} grow tooltip="Values should be in logfmt.">
|
||||
<Input
|
||||
id="tags"
|
||||
value={transformToLogfmt(query.tags)}
|
||||
placeholder="http.status_code=200 error=true"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
tags: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Min Duration" labelWidth={14} grow>
|
||||
<Input
|
||||
id="minDuration"
|
||||
name="minDuration"
|
||||
value={query.minDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
minDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Max Duration" labelWidth={14} grow>
|
||||
<Input
|
||||
id="maxDuration"
|
||||
name="maxDuration"
|
||||
value={query.maxDuration || ''}
|
||||
placeholder={durationPlaceholder}
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
maxDuration: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Limit" labelWidth={14} grow tooltip="Maximum number of returned results">
|
||||
<Input
|
||||
id="limit"
|
||||
name="limit"
|
||||
value={query.limit || ''}
|
||||
type="number"
|
||||
onChange={(v) =>
|
||||
onChange({
|
||||
...query,
|
||||
limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
{alertText && <TemporaryAlert text={alertText} severity="error" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { lastValueFrom, of, throwError } from 'rxjs';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
|
||||
import {
|
||||
DataQueryRequest,
|
||||
@ -10,11 +9,11 @@ import {
|
||||
PluginType,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { BackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
||||
import { JaegerDatasource, JaegerJsonData } from './datasource';
|
||||
import { createFetchResponse } from './helpers/createFetchResponse';
|
||||
import mockJson from './mockJsonResponse.json';
|
||||
import {
|
||||
testResponse,
|
||||
@ -24,6 +23,8 @@ import {
|
||||
} from './testResponse';
|
||||
import { JaegerQuery } from './types';
|
||||
|
||||
export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
@ -37,18 +38,16 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const timeSrvStub = {
|
||||
timeRange() {
|
||||
return {
|
||||
from: dateTime(1531468681),
|
||||
to: dateTime(1531489712),
|
||||
};
|
||||
},
|
||||
} as TimeSrv;
|
||||
|
||||
describe('JaegerDatasource', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const fetchMock = jest.spyOn(Date, 'now');
|
||||
fetchMock.mockImplementation(() => 1704106800000); // milliseconds for 2024-01-01 at 11:00am UTC
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns trace and graph when queried', async () => {
|
||||
@ -121,7 +120,7 @@ describe('JaegerDatasource', () => {
|
||||
|
||||
it('should return search results when the query type is search', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
@ -129,7 +128,7 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
expect(response.data[0].meta.preferredVisualisationType).toBe('table');
|
||||
// Make sure that traceID field has data link configured
|
||||
@ -138,7 +137,7 @@ describe('JaegerDatasource', () => {
|
||||
});
|
||||
|
||||
it('should show the correct error message if no service name is selected', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
@ -150,7 +149,7 @@ describe('JaegerDatasource', () => {
|
||||
|
||||
it('should remove operation from the query when all is selected', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
@ -158,13 +157,13 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert tags from logfmt format to an object', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
@ -172,13 +171,13 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve templates in traceID', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
@ -204,7 +203,7 @@ describe('JaegerDatasource', () => {
|
||||
|
||||
it('should resolve templates in tags', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
@ -218,13 +217,13 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should interpolate variables correctly', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const text = 'interpolationText';
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
@ -248,7 +247,7 @@ describe('JaegerDatasource', () => {
|
||||
})
|
||||
);
|
||||
expect(mock).toBeCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&start=1531468681000&end=1531489712000&lookback=custom`,
|
||||
url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -343,9 +342,9 @@ describe('Test behavior with unmocked time', () => {
|
||||
it("call for `query()` when `queryType === 'dependencyGraph'`", async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const now = Date.now();
|
||||
|
||||
ds.query({ ...defaultQuery, targets: [{ queryType: 'dependencyGraph', refId: '1' }] });
|
||||
const now = Date.now();
|
||||
|
||||
const url = mock.mock.calls[0][0].url;
|
||||
const endTsMatch = url.match(/endTs=(\d+)/);
|
||||
@ -354,7 +353,27 @@ describe('Test behavior with unmocked time', () => {
|
||||
|
||||
const lookbackMatch = url.match(/lookback=(\d+)/);
|
||||
expect(lookbackMatch).not.toBeNull();
|
||||
expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, numDigits);
|
||||
expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(3600000, -1); // due to rounding, the least significant digit is not reliable
|
||||
});
|
||||
|
||||
it("call for `query()` when `queryType === 'dependencyGraph'`, using default range", async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const now = Date.now();
|
||||
const query = JSON.parse(JSON.stringify(defaultQuery));
|
||||
// @ts-ignore
|
||||
query.range = undefined;
|
||||
|
||||
ds.query({ ...query, targets: [{ queryType: 'dependencyGraph', refId: '1' }] });
|
||||
|
||||
const url = mock.mock.calls[0][0].url;
|
||||
const endTsMatch = url.match(/endTs=(\d+)/);
|
||||
expect(endTsMatch).not.toBeNull();
|
||||
expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits);
|
||||
|
||||
const lookbackMatch = url.match(/lookback=(\d+)/);
|
||||
expect(lookbackMatch).not.toBeNull();
|
||||
expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, -1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -11,13 +11,13 @@ import {
|
||||
dateMath,
|
||||
DateTime,
|
||||
FieldType,
|
||||
getDefaultTimeRange,
|
||||
MutableDataFrame,
|
||||
ScopedVars,
|
||||
urlUtil,
|
||||
} from '@grafana/data';
|
||||
import { NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { BackendSrvRequest, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
||||
import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams';
|
||||
@ -39,7 +39,6 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
spanBar?: SpanBarOptions;
|
||||
constructor(
|
||||
private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv(),
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
@ -67,7 +66,7 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
|
||||
// Use the internal Jaeger /dependencies API for rendering the dependency graph.
|
||||
if (target.queryType === 'dependencyGraph') {
|
||||
const timeRange = this.timeSrv.timeRange();
|
||||
const timeRange = options.range ?? getDefaultTimeRange();
|
||||
const endTs = getTime(timeRange.to, true) / 1000;
|
||||
const lookback = endTs - getTime(timeRange.from, false) / 1000;
|
||||
return this._request('/api/dependencies', { endTs, lookback }).pipe(map(mapJaegerDependenciesResponse));
|
||||
@ -227,7 +226,7 @@ export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData>
|
||||
}
|
||||
|
||||
getTimeRange(): { start: number; end: number } {
|
||||
const range = this.timeSrv.timeRange();
|
||||
const range = getDefaultTimeRange();
|
||||
return {
|
||||
start: getTime(range.from, false),
|
||||
end: getTime(range.to, true),
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { FetchResponse } from '@grafana/runtime';
|
||||
|
||||
export function createFetchResponse<T>(data: T): FetchResponse<T> {
|
||||
return {
|
||||
data,
|
||||
status: 200,
|
||||
url: 'http://localhost:3000/api/ds/query',
|
||||
config: { url: 'http://localhost:3000/api/ds/query' },
|
||||
type: 'basic',
|
||||
statusText: 'Ok',
|
||||
redirected: false,
|
||||
headers: {} as unknown as Headers,
|
||||
ok: true,
|
||||
};
|
||||
}
|
43
public/app/plugins/datasource/jaeger/package.json
Normal file
43
public/app/plugins/datasource/jaeger/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@grafana-plugins/jaeger",
|
||||
"description": "Jaeger plugin for Grafana",
|
||||
"private": true,
|
||||
"version": "10.4.0-pre",
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.11.2",
|
||||
"@grafana/data": "workspace:*",
|
||||
"@grafana/experimental": "1.7.10",
|
||||
"@grafana/o11y-ds-frontend": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"lodash": "4.17.21",
|
||||
"logfmt": "^1.3.2",
|
||||
"react-window": "1.8.10",
|
||||
"rxjs": "7.8.1",
|
||||
"stream-browserify": "3.0.0",
|
||||
"tslib": "2.6.2",
|
||||
"uuid": "9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/plugin-configs": "workspace:*",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/react": "14.2.1",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/lodash": "4.17.0",
|
||||
"@types/logfmt": "^1.2.3",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/uuid": "9.0.8",
|
||||
"ts-node": "10.9.2",
|
||||
"webpack": "5.90.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -c ./webpack.config.ts --env production",
|
||||
"build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
|
||||
"dev": "webpack -w -c ./webpack.config.ts --env development"
|
||||
},
|
||||
"packageManager": "yarn@3.6.0"
|
||||
}
|
@ -30,6 +30,10 @@
|
||||
"name": "GitHub Project",
|
||||
"url": "https://github.com/jaegertracing/jaeger"
|
||||
}
|
||||
]
|
||||
],
|
||||
"version": "%VERSION%"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.3.0-0"
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
TraceLog,
|
||||
TraceSpanRow,
|
||||
} from '@grafana/data';
|
||||
import { transformTraceData } from 'app/features/explore/TraceView/components';
|
||||
|
||||
import transformTraceData from './_importedDependencies/model/transform-trace-data';
|
||||
import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types';
|
||||
|
||||
export function createTraceFrame(data: TraceResponse): DataFrame {
|
||||
|
7
public/app/plugins/datasource/jaeger/tsconfig.json
Normal file
7
public/app/plugins/datasource/jaeger/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "@testing-library/jest-dom"]
|
||||
},
|
||||
"extends": "@grafana/plugin-configs/tsconfig.json",
|
||||
"include": ["."]
|
||||
}
|
14
public/app/plugins/datasource/jaeger/webpack.config.ts
Normal file
14
public/app/plugins/datasource/jaeger/webpack.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import config from '@grafana/plugin-configs/webpack.config';
|
||||
|
||||
const configWithFallback = async (env: Record<string, unknown>) => {
|
||||
const response = await config(env);
|
||||
if (response !== undefined && response.resolve !== undefined) {
|
||||
response.resolve.fallback = {
|
||||
...response.resolve.fallback,
|
||||
stream: require.resolve('stream-browserify'),
|
||||
};
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export default configWithFallback;
|
Loading…
Reference in New Issue
Block a user