Remove jaegerBackendMigration feature toggle (#107702)
* remove feature toggle if statements * remove unused impoerts * remove unused private functions * prettier * official ft removal * fix some failing tests in datasource.test.ts * clean up test file * update test names * remove tests for testDatasource * remove describe * tests * fix import order * betterer
This commit is contained in:
+1
-2
@@ -3462,8 +3462,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/jaeger/datasource.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
||||
@@ -72,7 +72,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `pluginsSriChecks` | Enables SRI checks for plugin assets | |
|
||||
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
|
||||
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
|
||||
| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | Yes |
|
||||
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
|
||||
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
|
||||
| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes |
|
||||
|
||||
@@ -738,11 +738,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
crashDetection?: boolean;
|
||||
/**
|
||||
* Enables querying the Jaeger data source without the proxy
|
||||
* @default true
|
||||
*/
|
||||
jaegerBackendMigration?: boolean;
|
||||
/**
|
||||
* Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query
|
||||
* @default true
|
||||
*/
|
||||
|
||||
@@ -1261,13 +1261,6 @@ var (
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "jaegerBackendMigration",
|
||||
Description: "Enables querying the Jaeger data source without the proxy",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Expression: "true",
|
||||
},
|
||||
{
|
||||
Name: "alertingUIOptimizeReducer",
|
||||
Description: "Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query",
|
||||
|
||||
@@ -165,7 +165,6 @@ prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,fal
|
||||
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
|
||||
enableSCIM,preview,@grafana/identity-access-team,false,false,false
|
||||
crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||
jaegerBackendMigration,GA,@grafana/oss-big-tent,false,false,false
|
||||
alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true
|
||||
azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false
|
||||
alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
|
||||
|
@@ -671,10 +671,6 @@ const (
|
||||
// Enables browser crash detection reporting to Faro.
|
||||
FlagCrashDetection = "crashDetection"
|
||||
|
||||
// FlagJaegerBackendMigration
|
||||
// Enables querying the Jaeger data source without the proxy
|
||||
FlagJaegerBackendMigration = "jaegerBackendMigration"
|
||||
|
||||
// FlagAlertingUIOptimizeReducer
|
||||
// Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query
|
||||
FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer"
|
||||
|
||||
@@ -1542,6 +1542,7 @@
|
||||
"name": "jaegerBackendMigration",
|
||||
"resourceVersion": "1751465665226",
|
||||
"creationTimestamp": "2024-11-15T14:40:20Z",
|
||||
"deletionTimestamp": "2025-07-07T14:12:35Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-07-02 14:14:25.226989 +0000 UTC"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lastValueFrom, of, throwError } from 'rxjs';
|
||||
import { lastValueFrom, of } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataQueryRequest,
|
||||
@@ -9,18 +9,12 @@ import {
|
||||
PluginType,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { BackendSrv, config, DataSourceWithBackend } from '@grafana/runtime';
|
||||
import { BackendSrv, DataSourceWithBackend } 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,
|
||||
testResponseDataFrameFields,
|
||||
testResponseEdgesFields,
|
||||
testResponseNodesFields,
|
||||
} from './testResponse';
|
||||
import mockSearchResponse from './mockSearchResponse.json';
|
||||
import mockTraceResponse from './mockTraceResponse.json';
|
||||
import { JaegerQuery } from './types';
|
||||
|
||||
export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv;
|
||||
@@ -38,128 +32,29 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultQuery: DataQueryRequest<JaegerQuery> = {
|
||||
requestId: '1',
|
||||
interval: '0',
|
||||
intervalMs: 10,
|
||||
panelId: 0,
|
||||
scopedVars: {},
|
||||
range: {
|
||||
from: dateTime().subtract(1, 'h'),
|
||||
to: dateTime(),
|
||||
raw: { from: '1h', to: 'now' },
|
||||
},
|
||||
timezone: 'browser',
|
||||
app: 'explore',
|
||||
startTime: 0,
|
||||
targets: [
|
||||
{
|
||||
query: '12345',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('JaegerDatasource', () => {
|
||||
const defaultSearchRangeParams = `start=${Number(defaultQuery.range.from) * 1000}&end=${Number(defaultQuery.range.to) * 1000}`;
|
||||
|
||||
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 () => {
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(ds.query(defaultQuery));
|
||||
expect(response.data.length).toBe(3);
|
||||
expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields);
|
||||
expect(response.data[1].fields).toMatchObject(testResponseNodesFields);
|
||||
expect(response.data[2].fields).toMatchObject(testResponseEdgesFields);
|
||||
});
|
||||
|
||||
it('returns trace when traceId with special characters is queried', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
await lastValueFrom(ds.query(query));
|
||||
expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` });
|
||||
});
|
||||
|
||||
it('should trim whitespace from traceid', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b ',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
await lastValueFrom(ds.query(query));
|
||||
expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` });
|
||||
});
|
||||
|
||||
it('returns empty response if trace id is not specified', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
targets: [],
|
||||
})
|
||||
);
|
||||
const field = response.data[0].fields[0];
|
||||
expect(field.name).toBe('trace');
|
||||
expect(field.type).toBe(FieldType.trace);
|
||||
expect(field.values.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle json file upload', async () => {
|
||||
describe('upload, search and trace query types', () => {
|
||||
it('should process valid JSON file uploads', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
ds.uploadedJson = JSON.stringify(mockJson);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
targets: [{ queryType: 'upload', refId: 'A' }],
|
||||
})
|
||||
);
|
||||
const response = await lastValueFrom(ds.query({ ...defaultQuery, targets: [{ queryType: 'upload', refId: 'A' }] }));
|
||||
const field = response.data[0].fields[0];
|
||||
expect(field.name).toBe('traceID');
|
||||
expect(field.type).toBe(FieldType.string);
|
||||
expect(field.values.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should fail on invalid json file upload', async () => {
|
||||
it('should reject invalid JSON file uploads', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] });
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
targets: [{ queryType: 'upload', refId: 'A' }],
|
||||
} as DataQueryRequest<JaegerQuery>)
|
||||
ds.query({ targets: [{ queryType: 'upload', refId: 'A' }] } as DataQueryRequest<JaegerQuery>)
|
||||
);
|
||||
expect(response.error?.message).toBe('The JSON file uploaded is not in a valid Jaeger format');
|
||||
expect(response.data.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should return search results when the query type is search', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
it('should return search results when query type is search', async () => {
|
||||
setupQueryMock('search');
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
@@ -167,185 +62,66 @@ describe('JaegerDatasource', () => {
|
||||
targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: '/api/services' }],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&${defaultSearchRangeParams}&lookback=custom`,
|
||||
});
|
||||
|
||||
expect(response.data[0].meta.preferredVisualisationType).toBe('table');
|
||||
// Make sure that traceID field has data link configured
|
||||
expect(response.data[0].fields[0].config.links).toHaveLength(1);
|
||||
expect(response.data[0].fields[0].name).toBe('traceID');
|
||||
});
|
||||
|
||||
it('uses default range when no range is provided for search query,', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
it('should return trace results when query type is trace', async () => {
|
||||
setupQueryMock('trace');
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }],
|
||||
// set range to undefined to test default range
|
||||
range: undefined,
|
||||
} as unknown as DataQueryRequest<JaegerQuery>;
|
||||
const response = await lastValueFrom(
|
||||
ds.query({ ...defaultQuery, targets: [{ queryType: undefined, refId: 'a', query: '12345' }] })
|
||||
);
|
||||
|
||||
ds.query(query);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
// Check that query has time range from 6 hours ago to now (default range)
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1704085200000000&end=1704106800000000&lookback=custom`,
|
||||
});
|
||||
expect(response.data[0].meta.preferredVisualisationType).toBe('trace');
|
||||
expect(response.data[0].fields.length).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node graph functionality', () => {
|
||||
it('should include node graph frames when nodeGraph is enabled for trace queries', async () => {
|
||||
const settingsWithNodeGraph = {
|
||||
...defaultSettings,
|
||||
jsonData: {
|
||||
...defaultSettings.jsonData,
|
||||
nodeGraph: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
const ds = new JaegerDatasource(settingsWithNodeGraph);
|
||||
setupQueryMock('trace');
|
||||
|
||||
it('should show the correct error message if no service name is selected', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
targets: [{ queryType: 'search', refId: 'a', service: undefined, operation: '/api/services' }],
|
||||
})
|
||||
);
|
||||
expect(response.error?.message).toBe('You must select a service.');
|
||||
});
|
||||
|
||||
it('should remove operation from the query when all is selected', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&${defaultSearchRangeParams}&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert tags from logfmt format to an object', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=true' }],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve templates in traceID', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
scopedVars: {
|
||||
$traceid: {
|
||||
text: 'traceid',
|
||||
value: '5311b0dd0ca8df3463df93c99cb805a6',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
query: '$traceid',
|
||||
query: '12345',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces/5311b0dd0ca8df3463df93c99cb805a6`,
|
||||
});
|
||||
|
||||
expect(response.data.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should resolve templates in tags', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
await lastValueFrom(
|
||||
it('should exclude node graph frames when nodeGraph is disabled for trace queries', async () => {
|
||||
const settingsWithoutNodeGraph = {
|
||||
...defaultSettings,
|
||||
jsonData: {
|
||||
...defaultSettings.jsonData,
|
||||
nodeGraph: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
const ds = new JaegerDatasource(settingsWithoutNodeGraph);
|
||||
setupQueryMock('trace');
|
||||
|
||||
const response = await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
scopedVars: {
|
||||
'error=$error': {
|
||||
text: 'error',
|
||||
value: 'error=true',
|
||||
},
|
||||
},
|
||||
targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=$error' }],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should interpolate variables correctly', async () => {
|
||||
const mock = setupFetchMock({ data: [testResponse] });
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const text = 'interpolationText';
|
||||
await lastValueFrom(
|
||||
ds.query({
|
||||
...defaultQuery,
|
||||
scopedVars: {
|
||||
$interpolationVar: {
|
||||
text: text,
|
||||
value: text,
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
queryType: 'search',
|
||||
refId: 'a',
|
||||
service: '$interpolationVar',
|
||||
operation: '$interpolationVar',
|
||||
minDuration: '$interpolationVar',
|
||||
maxDuration: '$interpolationVar',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&${defaultSearchRangeParams}&lookback=custom`,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when jaegerBackendMigration feature toggle is enabled', () => {
|
||||
let originalFeatureToggleValue: boolean | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalFeatureToggleValue = config.featureToggles.jaegerBackendMigration;
|
||||
config.featureToggles.jaegerBackendMigration = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.featureToggles.jaegerBackendMigration = originalFeatureToggleValue;
|
||||
});
|
||||
|
||||
it('should add node graph frames to response when nodeGraph is enabled and query is a trace ID query', async () => {
|
||||
// Create a datasource with nodeGraph enabled
|
||||
const settings = {
|
||||
...defaultSettings,
|
||||
jsonData: {
|
||||
...defaultSettings.jsonData,
|
||||
nodeGraph: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
const ds = new JaegerDatasource(settings);
|
||||
|
||||
// Mock the super.query method to return our mock response
|
||||
jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => {
|
||||
return of({
|
||||
data: [
|
||||
{
|
||||
fields: testResponseDataFrameFields,
|
||||
values: testResponseDataFrameFields.values,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Create a query without queryType (trace ID query)
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
@@ -353,189 +129,31 @@ describe('JaegerDatasource', () => {
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the query
|
||||
const response = await lastValueFrom(ds.query(query));
|
||||
// Verify that the response contains the original data plus node graph frames
|
||||
expect(response.data.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should not add node graph frames when nodeGraph is disabled', async () => {
|
||||
// Create a datasource with nodeGraph disabled
|
||||
const settings = {
|
||||
...defaultSettings,
|
||||
jsonData: {
|
||||
...defaultSettings.jsonData,
|
||||
nodeGraph: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
const ds = new JaegerDatasource(settings);
|
||||
|
||||
// Mock the super.query method to return our mock response
|
||||
jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => {
|
||||
return of({
|
||||
data: [
|
||||
{
|
||||
fields: testResponseDataFrameFields,
|
||||
values: testResponseDataFrameFields.values,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// Create a query without queryType (trace ID query)
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: '12345',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Execute the query
|
||||
const response = await lastValueFrom(ds.query(query));
|
||||
// Verify that the response contains only the original data
|
||||
expect(response.data.length).toBe(1);
|
||||
expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields);
|
||||
});
|
||||
expect(response.data.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when performing testDataSource', () => {
|
||||
describe('and call succeeds', () => {
|
||||
it('should return successfully', async () => {
|
||||
setupFetchMock({ data: ['service1'] });
|
||||
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await ds.testDatasource();
|
||||
expect(response.status).toEqual('success');
|
||||
expect(response.message).toBe('Data source connected and services found.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and call succeeds, but returns no services', () => {
|
||||
it('should display an error', async () => {
|
||||
setupFetchMock(undefined);
|
||||
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await ds.testDatasource();
|
||||
expect(response.status).toEqual('error');
|
||||
expect(response.message).toBe(
|
||||
'Data source connected, but no services received. Verify that Jaeger is configured properly.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and call returns error with message', () => {
|
||||
it('should return the formatted error', async () => {
|
||||
setupFetchMock(
|
||||
undefined,
|
||||
throwError({
|
||||
statusText: 'Not found',
|
||||
status: 404,
|
||||
data: {
|
||||
message: '404 page not found',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await ds.testDatasource();
|
||||
expect(response.status).toEqual('error');
|
||||
expect(response.message).toBe('Jaeger: Not found. 404. 404 page not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and call returns error without message', () => {
|
||||
it('should return JSON error', async () => {
|
||||
setupFetchMock(
|
||||
undefined,
|
||||
throwError({
|
||||
statusText: 'Bad gateway',
|
||||
status: 502,
|
||||
data: {
|
||||
errors: ['Could not connect to Jaeger backend'],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const response = await ds.testDatasource();
|
||||
expect(response.status).toEqual('error');
|
||||
expect(response.message).toBe('Jaeger: Bad gateway. 502. {"errors":["Could not connect to Jaeger backend"]}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test behavior with unmocked time', () => {
|
||||
// Tolerance for checking timestamps.
|
||||
// Using a lower number seems to cause flaky tests.
|
||||
const numDigits = -4;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('getTimeRange()', async () => {
|
||||
describe('time range', () => {
|
||||
it('should calculate correct time range', async () => {
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
const timeRange = ds.getTimeRange();
|
||||
const now = Date.now();
|
||||
expect(timeRange.end).toBeCloseTo(now * 1000, numDigits);
|
||||
expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, numDigits);
|
||||
});
|
||||
|
||||
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+)/);
|
||||
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);
|
||||
expect(timeRange.end).toBeCloseTo(now * 1000, -4);
|
||||
expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, -4);
|
||||
});
|
||||
});
|
||||
|
||||
function setupFetchMock(response: unknown, mock?: ReturnType<typeof backendSrv.fetch>) {
|
||||
const defaultMock = () => mock ?? of(createFetchResponse(response));
|
||||
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
fetchMock.mockImplementation(defaultMock);
|
||||
return fetchMock;
|
||||
function setupQueryMock(type: 'trace' | 'search') {
|
||||
return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => {
|
||||
if (type === 'search') {
|
||||
return of(mockSearchResponse);
|
||||
} else {
|
||||
return of(mockTraceResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
|
||||
@@ -560,3 +178,25 @@ const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
|
||||
},
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
const defaultQuery: DataQueryRequest<JaegerQuery> = {
|
||||
requestId: '1',
|
||||
interval: '0',
|
||||
intervalMs: 10,
|
||||
panelId: 0,
|
||||
scopedVars: {},
|
||||
range: {
|
||||
from: dateTime().subtract(1, 'h'),
|
||||
to: dateTime(),
|
||||
raw: { from: '1h', to: 'now' },
|
||||
},
|
||||
timezone: 'browser',
|
||||
app: 'explore',
|
||||
startTime: 0,
|
||||
targets: [
|
||||
{
|
||||
query: '12345',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { identity, omit, pick, pickBy } from 'lodash';
|
||||
import { lastValueFrom, Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
DataQueryRequest,
|
||||
@@ -14,25 +13,14 @@ import {
|
||||
MutableDataFrame,
|
||||
ScopedVars,
|
||||
toDataFrame,
|
||||
urlUtil,
|
||||
} from '@grafana/data';
|
||||
import { createNodeGraphFrames, NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend';
|
||||
import {
|
||||
BackendSrvRequest,
|
||||
config,
|
||||
DataSourceWithBackend,
|
||||
getBackendSrv,
|
||||
getTemplateSrv,
|
||||
TemplateSrv,
|
||||
} from '@grafana/runtime';
|
||||
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { ALL_OPERATIONS_KEY } from './components/SearchForm';
|
||||
import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams';
|
||||
import { mapJaegerDependenciesResponse } from './dependencyGraphTransform';
|
||||
import { createGraphFrames } from './graphTransform';
|
||||
import { createTableFrame, createTraceFrame } from './responseTransform';
|
||||
import { createTraceFrame } from './responseTransform';
|
||||
import { JaegerQuery } from './types';
|
||||
import { convertTagsLogfmt } from './util';
|
||||
|
||||
export interface JaegerJsonData extends DataSourceJsonData {
|
||||
nodeGraph?: NodeGraphOptions;
|
||||
@@ -45,7 +33,7 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
traceIdTimeParams?: TraceIdTimeParamsOptions;
|
||||
spanBar?: SpanBarOptions;
|
||||
constructor(
|
||||
private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
|
||||
instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
@@ -53,25 +41,14 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
this.traceIdTimeParams = instanceSettings.jsonData.traceIdTimeParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrated to backend with feature toggle `jaegerBackendMigration`
|
||||
*/
|
||||
async metadataRequest(url: string, params?: Record<string, unknown>) {
|
||||
if (config.featureToggles.jaegerBackendMigration) {
|
||||
return await this.getResource(url, params);
|
||||
}
|
||||
|
||||
const res = await lastValueFrom(this._request('/api/' + url, params, { hideFromInspector: true }));
|
||||
return res.data.data;
|
||||
return await this.getResource(url, params);
|
||||
}
|
||||
|
||||
isSearchFormValid(query: JaegerQuery): boolean {
|
||||
return !!query.service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrated to backend with feature toggle `jaegerBackendMigration`
|
||||
*/
|
||||
query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> {
|
||||
// At this moment we expect only one target. In case we somehow change the UI to be able to show multiple
|
||||
// traces at one we need to change this.
|
||||
@@ -80,55 +57,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
return of({ data: [emptyTraceDataFrame] });
|
||||
}
|
||||
|
||||
if (config.featureToggles.jaegerBackendMigration && target.queryType !== 'upload') {
|
||||
return super.query({ ...options, targets: [target] }).pipe(
|
||||
map((response) => {
|
||||
// If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response
|
||||
if (this.nodeGraph?.enabled && !target.queryType) {
|
||||
return addNodeGraphFramesToResponse(response);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Use the internal Jaeger /dependencies API for rendering the dependency graph.
|
||||
if (target.queryType === 'dependencyGraph') {
|
||||
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));
|
||||
}
|
||||
|
||||
if (target.queryType === 'search' && !this.isSearchFormValid(target)) {
|
||||
return of({ error: { message: 'You must select a service.' }, data: [] });
|
||||
}
|
||||
|
||||
let { start, end } = this.getTimeRange(options.range);
|
||||
|
||||
if (target.queryType !== 'search' && target.query) {
|
||||
let url = `/api/traces/${encodeURIComponent(this.templateSrv.replace(target.query.trim(), options.scopedVars))}`;
|
||||
if (this.traceIdTimeParams) {
|
||||
url += `?start=${start}&end=${end}`;
|
||||
}
|
||||
|
||||
return this._request(url).pipe(
|
||||
map((response) => {
|
||||
const traceData = response?.data?.data?.[0];
|
||||
if (!traceData) {
|
||||
return { data: [emptyTraceDataFrame] };
|
||||
}
|
||||
let data = [createTraceFrame(traceData)];
|
||||
if (this.nodeGraph?.enabled) {
|
||||
data.push(...createGraphFrames(traceData));
|
||||
}
|
||||
return {
|
||||
data,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (target.queryType === 'upload') {
|
||||
if (!this.uploadedJson) {
|
||||
return of({ data: [] });
|
||||
@@ -146,38 +74,13 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
}
|
||||
}
|
||||
|
||||
let jaegerInterpolated = pick(this.applyTemplateVariables(target, options.scopedVars), [
|
||||
'service',
|
||||
'operation',
|
||||
'tags',
|
||||
'minDuration',
|
||||
'maxDuration',
|
||||
'limit',
|
||||
]);
|
||||
// remove empty properties
|
||||
let jaegerQuery = pickBy(jaegerInterpolated, identity);
|
||||
|
||||
if (jaegerQuery.operation === ALL_OPERATIONS_KEY) {
|
||||
jaegerQuery = omit(jaegerQuery, 'operation');
|
||||
}
|
||||
|
||||
if (jaegerQuery.tags) {
|
||||
jaegerQuery = {
|
||||
...jaegerQuery,
|
||||
tags: convertTagsLogfmt(jaegerQuery.tags.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used.
|
||||
return this._request(`/api/traces`, {
|
||||
...jaegerQuery,
|
||||
...this.getTimeRange(options.range),
|
||||
lookback: 'custom',
|
||||
}).pipe(
|
||||
return super.query({ ...options, targets: [target] }).pipe(
|
||||
map((response) => {
|
||||
return {
|
||||
data: [createTableFrame(response.data.data, this.instanceSettings)],
|
||||
};
|
||||
// If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response
|
||||
if (this.nodeGraph?.enabled && !target.queryType) {
|
||||
return addNodeGraphFramesToResponse(response);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -215,49 +118,8 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrated to backend with feature toggle `jaegerBackendMigration`
|
||||
*/
|
||||
async testDatasource() {
|
||||
if (config.featureToggles.jaegerBackendMigration) {
|
||||
return await super.testDatasource();
|
||||
}
|
||||
|
||||
return lastValueFrom(
|
||||
this._request('/api/services').pipe(
|
||||
map((res) => {
|
||||
const values = res?.data?.data || [];
|
||||
const testResult =
|
||||
values.length > 0
|
||||
? { status: 'success', message: 'Data source connected and services found.' }
|
||||
: {
|
||||
status: 'error',
|
||||
message:
|
||||
'Data source connected, but no services received. Verify that Jaeger is configured properly.',
|
||||
};
|
||||
return testResult;
|
||||
}),
|
||||
catchError((err) => {
|
||||
let message = 'Jaeger: ';
|
||||
if (err.statusText) {
|
||||
message += err.statusText;
|
||||
} else {
|
||||
message += 'Cannot connect to Jaeger';
|
||||
}
|
||||
|
||||
if (err.status) {
|
||||
message += `. ${err.status}`;
|
||||
}
|
||||
|
||||
if (err.data && err.data.message) {
|
||||
message += `. ${err.data.message}`;
|
||||
} else if (err.data) {
|
||||
message += `. ${JSON.stringify(err.data)}`;
|
||||
}
|
||||
return of({ status: 'error', message: message });
|
||||
})
|
||||
)
|
||||
);
|
||||
return await super.testDatasource();
|
||||
}
|
||||
|
||||
getTimeRange(range = getDefaultTimeRange()): { start: number; end: number } {
|
||||
@@ -270,21 +132,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
|
||||
getQueryDisplayText(query: JaegerQuery) {
|
||||
return query.query || '';
|
||||
}
|
||||
|
||||
private _request(
|
||||
apiUrl: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: Partial<BackendSrvRequest>
|
||||
): Observable<Record<string, any>> {
|
||||
const params = data ? urlUtil.serializeParams(data) : '';
|
||||
const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`;
|
||||
const req = {
|
||||
...options,
|
||||
url,
|
||||
};
|
||||
|
||||
return getBackendSrv().fetch(req);
|
||||
}
|
||||
}
|
||||
|
||||
function getTime(date: string | DateTime, roundUp: boolean) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"name": "traceID",
|
||||
"values": ["test-trace-id"],
|
||||
"config": {
|
||||
"displayName": "Trace ID",
|
||||
"links": [
|
||||
{
|
||||
"title": "Trace: ${__value.raw}",
|
||||
"internal": {
|
||||
"query": {
|
||||
"query": "${__value.raw}"
|
||||
},
|
||||
"datasourceUid": "test-uid",
|
||||
"datasourceName": "test-name"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "traceName",
|
||||
"values": ["test-service: test-operation"],
|
||||
"config": {
|
||||
"displayName": "Trace name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startTime",
|
||||
"values": [1605873894680],
|
||||
"config": {
|
||||
"displayName": "Start time"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
"values": [1000],
|
||||
"config": {
|
||||
"displayName": "Duration",
|
||||
"unit": "µs"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"preferredVisualisationType": "table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"fields": [
|
||||
{ "name": "traceID", "values": ["3fa414edcef6ad90", "3fa414edcef6ad90"] },
|
||||
{ "name": "spanID", "values": ["3fa414edcef6ad90", "0f5c1808567e4403"] },
|
||||
{ "name": "parentSpanID", "values": [null, "3fa414edcef6ad90"] },
|
||||
{ "name": "operationName", "values": ["HTTP GET - api_traces_traceid", "/tempopb.Querier/FindTraceByID"] },
|
||||
{ "name": "serviceName", "values": ["tempo-querier", "tempo-querier"] },
|
||||
{ "name": "startTime", "values": [1605873894680.409, 1605873894680.587] },
|
||||
{ "name": "duration", "values": [1049.141, 1.847] }
|
||||
],
|
||||
"meta": {
|
||||
"preferredVisualisationType": "trace",
|
||||
"custom": {
|
||||
"traceFormat": "jaeger"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user