Jaeger: Decouple Jaeger plugin (#81377)

pull/84988/head
Fabrizio 1 year ago committed by GitHub
parent 83597e6c37
commit d1f791cf1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      .betterer.results
  2. 14
      .eslintrc
  3. 1
      .github/workflows/core-plugins-build-and-release.yml
  4. 2
      pkg/tests/api/plugins/data/expectedListResp.json
  5. 3
      public/app/features/plugins/built_in_plugins.ts
  6. 1
      public/app/plugins/datasource/jaeger/CHANGELOG.md
  7. 8
      public/app/plugins/datasource/jaeger/README.md
  8. 3
      public/app/plugins/datasource/jaeger/_importedDependencies/README.md
  9. 57
      public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts
  10. 199
      public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx
  11. 65
      public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts
  12. 23
      public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx
  13. 112
      public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts
  14. 139
      public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts
  15. 40
      public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts
  16. 29
      public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx
  17. 6
      public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
  18. 241
      public/app/plugins/datasource/jaeger/components/SearchForm.tsx
  19. 69
      public/app/plugins/datasource/jaeger/datasource.test.ts
  20. 7
      public/app/plugins/datasource/jaeger/datasource.ts
  21. 15
      public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts
  22. 43
      public/app/plugins/datasource/jaeger/package.json
  23. 6
      public/app/plugins/datasource/jaeger/plugin.json
  24. 2
      public/app/plugins/datasource/jaeger/responseTransform.ts
  25. 7
      public/app/plugins/datasource/jaeger/tsconfig.json
  26. 14
      public/app/plugins/datasource/jaeger/webpack.config.ts
  27. 1454
      yarn.lock

@ -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"]

@ -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,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,29 @@ 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 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(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+)/);
@ -354,7 +373,7 @@ 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(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,
};
}

@ -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 {

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": ["jest", "@testing-library/jest-dom"]
},
"extends": "@grafana/plugin-configs/tsconfig.json",
"include": ["."]
}

@ -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;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save