mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ExploreMetrics: Refactor logs integration to prepare for additional association techniques (#98430)
* refactor: extract implementation details * chore: remove unused code * refactor: prefer interfaces * docs: add clarity
This commit is contained in:
parent
e974cb87d8
commit
81ca734680
28
public/app/features/trails/Integrations/logs/base.ts
Normal file
28
public/app/features/trails/Integrations/logs/base.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
|
||||
export type FoundLokiDataSource = Pick<DataSourceSettings, 'name' | 'uid'>;
|
||||
|
||||
/**
|
||||
* Defines the interface for connecting metrics and their related logs.
|
||||
* Implementations should provide methods for retrieving Loki data sources associated
|
||||
* with a metric, and creating a Loki query expression for a given metric and data source.
|
||||
*
|
||||
* By using this interface, the `RelatedLogsScene` can orchestrate
|
||||
* the retrieval of logs without needing to know the specifics of how we're
|
||||
* associating logs with a given metric.
|
||||
*/
|
||||
export interface MetricsLogsConnector {
|
||||
/**
|
||||
* Retrieves the Loki data sources associated with the specified metric.
|
||||
*/
|
||||
getDataSources(selectedMetric: string): Promise<FoundLokiDataSource[]>;
|
||||
|
||||
/**
|
||||
* Constructs a Loki query expression for the specified metric and data source.
|
||||
*/
|
||||
getLokiQueryExpr(selectedMetric: string, datasourceUid: string): string;
|
||||
}
|
||||
|
||||
export function createMetricsLogsConnector<T extends MetricsLogsConnector>(connector: T): T {
|
||||
return connector;
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import type { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import * as runtime from '@grafana/runtime';
|
||||
|
||||
import { MetricsLogsConnector } from './base';
|
||||
import { lokiRecordingRulesConnector, type RecordingRuleGroup } from './lokiRecordingRules';
|
||||
|
||||
const mockLokiDS1: DataSourceInstanceSettings<DataSourceJsonData> = {
|
||||
access: 'proxy',
|
||||
id: 1,
|
||||
uid: 'loki1',
|
||||
name: 'Loki Main',
|
||||
type: 'loki',
|
||||
url: '',
|
||||
jsonData: {},
|
||||
meta: {
|
||||
...getMockPlugin(),
|
||||
id: 'loki',
|
||||
},
|
||||
readOnly: false,
|
||||
isDefault: false,
|
||||
database: '',
|
||||
withCredentials: false,
|
||||
};
|
||||
|
||||
const mockLokiDS2: DataSourceInstanceSettings<DataSourceJsonData> = {
|
||||
...mockLokiDS1,
|
||||
id: 2,
|
||||
uid: 'loki2',
|
||||
name: 'Loki Secondary',
|
||||
};
|
||||
|
||||
const mockRuleGroups1: RecordingRuleGroup[] = [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_a_total',
|
||||
query: 'sum(rate({app="app-A"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
{
|
||||
name: 'metric_b_total',
|
||||
query: 'sum(rate({app="app-B"} |= "warn" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockRuleGroups2: RecordingRuleGroup[] = [
|
||||
{
|
||||
name: 'group2',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_a_total', // Intentionally same name as in DS1
|
||||
query: 'sum(rate({app="app-C"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Create spy functions
|
||||
const getListSpy = jest.fn().mockReturnValue([mockLokiDS1, mockLokiDS2]);
|
||||
const defaultFetchImpl = (req: Request) => {
|
||||
if (req.url.includes('loki1')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups1 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups2 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
};
|
||||
const fetchSpy = jest.fn().mockImplementation(defaultFetchImpl);
|
||||
|
||||
// Mock the entire @grafana/runtime module
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
getList: getListSpy,
|
||||
get: jest.fn(),
|
||||
getInstanceSettings: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}),
|
||||
getBackendSrv: () => ({
|
||||
fetch: fetchSpy,
|
||||
delete: jest.fn(),
|
||||
get: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
request: jest.fn(),
|
||||
datasourceRequest: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('LokiRecordingRulesConnector', () => {
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
getListSpy.mockClear();
|
||||
fetchSpy.mockClear();
|
||||
fetchSpy.mockImplementation(defaultFetchImpl);
|
||||
consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('getDataSources', () => {
|
||||
it('should find all data sources containing the metric', async () => {
|
||||
const connector = lokiRecordingRulesConnector;
|
||||
const result = await connector.getDataSources('metric_a_total');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual({ name: 'Loki Main', uid: 'loki1' });
|
||||
expect(result).toContainEqual({ name: 'Loki Secondary', uid: 'loki2' });
|
||||
|
||||
// Verify underlying calls
|
||||
expect(getListSpy).toHaveBeenCalledWith({ logs: true });
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle non-existent metrics', async () => {
|
||||
const connector = lokiRecordingRulesConnector;
|
||||
const result = await connector.getDataSources('non_existent_metric');
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle datasource fetch errors gracefully', async () => {
|
||||
// Make the second datasource fail
|
||||
fetchSpy.mockImplementation((req) => {
|
||||
if (req.url.includes('loki1')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups1 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
throw new Error('Failed to fetch');
|
||||
});
|
||||
|
||||
const connector = lokiRecordingRulesConnector;
|
||||
const result = await connector.getDataSources('metric_a_total');
|
||||
|
||||
// Should still get results from the working datasource
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ name: 'Loki Main', uid: 'loki1' });
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLokiQueryExpr', () => {
|
||||
let connector: MetricsLogsConnector;
|
||||
|
||||
beforeEach(async () => {
|
||||
connector = lokiRecordingRulesConnector;
|
||||
// Populate the rules first
|
||||
await connector.getDataSources('metric_a_total');
|
||||
});
|
||||
|
||||
it('should return correct Loki query for existing metric', () => {
|
||||
const result = connector.getLokiQueryExpr('metric_a_total', 'loki1');
|
||||
expect(result).toBe('{app="app-A"} |= "error"');
|
||||
});
|
||||
|
||||
it('should return empty string for non-existent metric', () => {
|
||||
const result = connector.getLokiQueryExpr('non_existent_metric', 'loki1');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle multiple occurrences of the same metric name', () => {
|
||||
const query1 = connector.getLokiQueryExpr('metric_a_total', 'loki1');
|
||||
const query2 = connector.getLokiQueryExpr('metric_a_total', 'loki2');
|
||||
|
||||
expect(query1).toBe('{app="app-A"} |= "error"');
|
||||
expect(query2).toBe('{app="app-C"} |= "error"');
|
||||
});
|
||||
|
||||
it('should handle rules with hasMultipleOccurrences flag', () => {
|
||||
const query = connector.getLokiQueryExpr('metric_a_total', 'loki1');
|
||||
expect(query).toBe('{app="app-A"} |= "error"');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,29 +1,31 @@
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import type { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import type { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { getBackendSrv, getDataSourceSrv, type BackendSrvRequest, type FetchResponse } from '@grafana/runtime';
|
||||
import { getLogQueryFromMetricsQuery } from 'app/plugins/datasource/loki/queryUtils';
|
||||
|
||||
export type RecordingRuleGroup = {
|
||||
import { createMetricsLogsConnector, type FoundLokiDataSource } from './base';
|
||||
|
||||
export interface RecordingRuleGroup {
|
||||
name: string;
|
||||
rules: RecordingRule[];
|
||||
};
|
||||
}
|
||||
|
||||
export type RecordingRule = {
|
||||
export interface RecordingRule {
|
||||
name: string;
|
||||
query: string;
|
||||
type: 'recording' | 'alerting' | string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
export type FoundLokiDataSource = Pick<DataSourceSettings, 'name' | 'uid'>;
|
||||
export type ExtractedRecordingRule = RecordingRule & {
|
||||
export interface ExtractedRecordingRule extends RecordingRule {
|
||||
datasource: FoundLokiDataSource;
|
||||
hasMultipleOccurrences?: boolean;
|
||||
};
|
||||
export type ExtractedRecordingRules = {
|
||||
}
|
||||
|
||||
export interface ExtractedRecordingRules {
|
||||
[dataSourceUID: string]: ExtractedRecordingRule[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Loki recording rule groups from the specified datasource.
|
||||
@ -60,7 +62,6 @@ export function extractRecordingRulesFromRuleGroups(
|
||||
|
||||
// We only want to return the first matching rule when there are multiple rules with same name
|
||||
const extractedRules = new Map<string, ExtractedRecordingRule>();
|
||||
// const extractedRules: [] = [];
|
||||
ruleGroups.forEach((rg) => {
|
||||
rg.rules
|
||||
.filter((r) => r.type === 'recording')
|
||||
@ -167,3 +168,22 @@ export async function fetchAndExtractLokiRecordingRules() {
|
||||
|
||||
return extractedRecordingRules;
|
||||
}
|
||||
|
||||
const createLokiRecordingRulesConnector = () => {
|
||||
let lokiRecordingRules: ExtractedRecordingRules = {};
|
||||
|
||||
return createMetricsLogsConnector({
|
||||
async getDataSources(selectedMetric: string): Promise<FoundLokiDataSource[]> {
|
||||
lokiRecordingRules = await fetchAndExtractLokiRecordingRules();
|
||||
const lokiDataSources = getDataSourcesWithRecordingRulesContainingMetric(selectedMetric, lokiRecordingRules);
|
||||
|
||||
return lokiDataSources;
|
||||
},
|
||||
|
||||
getLokiQueryExpr(selectedMetric: string, datasourceUid: string): string {
|
||||
return getLokiQueryForRelatedMetric(selectedMetric, datasourceUid, lokiRecordingRules);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const lokiRecordingRulesConnector = createLokiRecordingRulesConnector();
|
@ -1,311 +0,0 @@
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import type { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { getMockPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import * as runtime from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
extractRecordingRulesFromRuleGroups,
|
||||
fetchAndExtractLokiRecordingRules,
|
||||
getLokiQueryForRelatedMetric,
|
||||
getDataSourcesWithRecordingRulesContainingMetric,
|
||||
type ExtractedRecordingRules,
|
||||
type RecordingRuleGroup,
|
||||
} from './logsIntegration';
|
||||
|
||||
const mockLokiDS1: DataSourceInstanceSettings<DataSourceJsonData> = {
|
||||
access: 'proxy',
|
||||
id: 1,
|
||||
uid: 'loki1',
|
||||
name: 'Loki Main',
|
||||
type: 'loki',
|
||||
url: '',
|
||||
jsonData: {},
|
||||
meta: {
|
||||
...getMockPlugin(),
|
||||
id: 'loki',
|
||||
},
|
||||
readOnly: false,
|
||||
isDefault: false,
|
||||
database: '',
|
||||
withCredentials: false,
|
||||
};
|
||||
|
||||
const mockLokiDS2: DataSourceInstanceSettings<DataSourceJsonData> = {
|
||||
...mockLokiDS1,
|
||||
id: 2,
|
||||
uid: 'loki2',
|
||||
name: 'Loki Secondary',
|
||||
};
|
||||
|
||||
const mockLokiDS3: DataSourceInstanceSettings<DataSourceJsonData> = {
|
||||
...mockLokiDS1,
|
||||
id: 3,
|
||||
uid: 'loki3',
|
||||
name: 'Loki the Third with same rules',
|
||||
};
|
||||
|
||||
const mockLokiDSs = [mockLokiDS1, mockLokiDS2, mockLokiDS3];
|
||||
|
||||
const mockRuleGroups1: RecordingRuleGroup[] = [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_a_total',
|
||||
query: 'sum(rate({app="app-A"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
{
|
||||
name: 'metric_b_total',
|
||||
query: 'sum(rate({app="app-B"} |= "warn" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockRuleGroups2: RecordingRuleGroup[] = [
|
||||
{
|
||||
name: 'group2',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_a_total', // Intentionally same name as in DS1
|
||||
query: 'sum(rate({app="app-C"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockRuleGroupsWithSameRuleName: RecordingRuleGroup[] = [
|
||||
{
|
||||
name: 'group_with_same_rule_names',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_xx_total',
|
||||
query: 'sum(rate({app="app-XX"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
labels: {
|
||||
customLabel: 'label value 5m',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'metric_xx_total',
|
||||
query: 'sum(rate({app="app-YY"} |= "warn" [10m]))',
|
||||
type: 'recording',
|
||||
labels: {
|
||||
customLabel: 'label value 10m',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockExtractedRules: ExtractedRecordingRules = {
|
||||
loki1: [
|
||||
{
|
||||
name: 'metric_a_total',
|
||||
query: 'sum(rate({app="app-A"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
datasource: { name: 'Loki Main', uid: 'loki1' },
|
||||
hasMultipleOccurrences: false,
|
||||
},
|
||||
{
|
||||
name: 'metric_b_total',
|
||||
query: 'sum(rate({app="app-B"} |= "warn" [5m]))',
|
||||
type: 'recording',
|
||||
datasource: { name: 'Loki Main', uid: 'loki1' },
|
||||
hasMultipleOccurrences: false,
|
||||
},
|
||||
],
|
||||
loki2: [
|
||||
{
|
||||
name: 'metric_a_total',
|
||||
query: 'sum(rate({app="app-C"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
datasource: { name: 'Loki Secondary', uid: 'loki2' },
|
||||
hasMultipleOccurrences: false,
|
||||
},
|
||||
],
|
||||
loki3: [
|
||||
{
|
||||
name: 'metric_xx_total',
|
||||
query: 'sum(rate({app="app-XX"} |= "error" [5m]))',
|
||||
type: 'recording',
|
||||
datasource: { name: 'Loki the Third with same rules', uid: 'loki3' },
|
||||
hasMultipleOccurrences: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Create spy functions
|
||||
const getListSpy = jest.fn().mockReturnValue(mockLokiDSs);
|
||||
const fetchSpy = jest.fn().mockImplementation((req) => {
|
||||
if (req.url.includes('loki1')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups1 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
if (req.url.includes('loki2')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups2 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
if (req.url.includes('loki3')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroupsWithSameRuleName } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
return of({
|
||||
data: { data: { groups: [] } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
});
|
||||
|
||||
// Mock the entire @grafana/runtime module
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
getList: getListSpy,
|
||||
get: jest.fn(),
|
||||
getInstanceSettings: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}),
|
||||
getBackendSrv: () => ({
|
||||
fetch: fetchSpy,
|
||||
delete: jest.fn(),
|
||||
get: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
request: jest.fn(),
|
||||
datasourceRequest: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Logs Integration', () => {
|
||||
describe('fetchAndExtractLokiRecordingRules', () => {
|
||||
beforeEach(() => {
|
||||
getListSpy.mockClear();
|
||||
fetchSpy.mockClear();
|
||||
});
|
||||
|
||||
it('should fetch and extract rules from all Loki data sources', async () => {
|
||||
const result = await fetchAndExtractLokiRecordingRules();
|
||||
|
||||
expect(result).toEqual(mockExtractedRules);
|
||||
expect(getListSpy).toHaveBeenCalledWith({ logs: true });
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(mockLokiDSs.length);
|
||||
});
|
||||
|
||||
it('should handle errors from individual data sources gracefully', async () => {
|
||||
// Mock console.error to avoid test output pollution
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
fetchSpy.mockImplementation((req) => {
|
||||
if (req.url.includes('loki1')) {
|
||||
return of({
|
||||
data: { data: { groups: mockRuleGroups1 } },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
redirected: false,
|
||||
type: 'basic',
|
||||
url: req.url,
|
||||
config: { url: req.url },
|
||||
} as runtime.FetchResponse);
|
||||
}
|
||||
throw new Error('Failed to fetch');
|
||||
});
|
||||
|
||||
const result = await fetchAndExtractLokiRecordingRules();
|
||||
|
||||
// Should still have results from the first datasource
|
||||
expect(result).toHaveProperty('loki1');
|
||||
expect(result.loki1).toHaveLength(2);
|
||||
expect(result.loki2).toBeUndefined();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLokiQueryForRelatedMetric', () => {
|
||||
it('should return the expected Loki query for a given metric', () => {
|
||||
const result = getLokiQueryForRelatedMetric('metric_a_total', 'loki1', mockExtractedRules);
|
||||
expect(result).toBe('{app="app-A"} |= "error"');
|
||||
});
|
||||
|
||||
it('should return empty string for non-existent data source', () => {
|
||||
const result = getLokiQueryForRelatedMetric('metric_a_total', 'non-existent', mockExtractedRules);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string for non-existent metric', () => {
|
||||
const result = getLokiQueryForRelatedMetric('non_existent_metric', 'loki1', mockExtractedRules);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataSourcesWithRecordingRulesContainingMetric', () => {
|
||||
it('should find all data sources containing recording rules that define the metric of interest', () => {
|
||||
const result = getDataSourcesWithRecordingRulesContainingMetric('metric_a_total', mockExtractedRules);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual({ name: 'Loki Main', uid: 'loki1' });
|
||||
expect(result).toContainEqual({ name: 'Loki Secondary', uid: 'loki2' });
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent metric', () => {
|
||||
const result = getDataSourcesWithRecordingRulesContainingMetric('non_existent_metric', mockExtractedRules);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should find single data source for unique metric', () => {
|
||||
const result = getDataSourcesWithRecordingRulesContainingMetric('metric_b_total', mockExtractedRules);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ name: 'Loki Main', uid: 'loki1' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractRecordingRulesFromRuleGroups', () => {
|
||||
it('should extract only the first rule from a rule group with same rule names', () => {
|
||||
const result = extractRecordingRulesFromRuleGroups(mockRuleGroupsWithSameRuleName, mockLokiDS3);
|
||||
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result).toEqual(mockExtractedRules.loki3);
|
||||
});
|
||||
});
|
@ -18,12 +18,8 @@ import {
|
||||
import { Stack, LinkButton } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import {
|
||||
fetchAndExtractLokiRecordingRules,
|
||||
getLokiQueryForRelatedMetric,
|
||||
getDataSourcesWithRecordingRulesContainingMetric,
|
||||
type ExtractedRecordingRules,
|
||||
} from '../Integrations/logsIntegration';
|
||||
import { MetricsLogsConnector } from '../Integrations/logs/base';
|
||||
import { lokiRecordingRulesConnector } from '../Integrations/logs/lokiRecordingRules';
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
import { VAR_LOGS_DATASOURCE, VAR_LOGS_DATASOURCE_EXPR, VAR_METRIC_EXPR } from '../shared';
|
||||
|
||||
@ -32,7 +28,7 @@ import { NoRelatedLogsScene } from './NoRelatedLogsFoundScene';
|
||||
export interface RelatedLogsSceneState extends SceneObjectState {
|
||||
controls: SceneObject[];
|
||||
body: SceneFlexLayout;
|
||||
lokiRecordingRules: ExtractedRecordingRules;
|
||||
connectors: MetricsLogsConnector[];
|
||||
}
|
||||
|
||||
const LOGS_PANEL_CONTAINER_KEY = 'related_logs/logs_panel_container';
|
||||
@ -52,7 +48,7 @@ export class RelatedLogsScene extends SceneObjectBase<RelatedLogsSceneState> {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
lokiRecordingRules: {},
|
||||
connectors: [lokiRecordingRulesConnector],
|
||||
...state,
|
||||
});
|
||||
|
||||
@ -60,12 +56,12 @@ export class RelatedLogsScene extends SceneObjectBase<RelatedLogsSceneState> {
|
||||
}
|
||||
|
||||
private onActivate() {
|
||||
fetchAndExtractLokiRecordingRules().then((lokiRecordingRules) => {
|
||||
const selectedMetric = sceneGraph.interpolate(this, VAR_METRIC_EXPR);
|
||||
const lokiDatasources = getDataSourcesWithRecordingRulesContainingMetric(selectedMetric, lokiRecordingRules);
|
||||
const selectedMetric = sceneGraph.interpolate(this, VAR_METRIC_EXPR);
|
||||
Promise.all(this.state.connectors.map((connector) => connector.getDataSources(selectedMetric))).then((results) => {
|
||||
const lokiDataSources = results.flat();
|
||||
const logsPanelContainer = sceneGraph.findByKeyAndType(this, LOGS_PANEL_CONTAINER_KEY, SceneFlexItem);
|
||||
|
||||
if (!lokiDatasources?.length) {
|
||||
if (!lokiDataSources?.length) {
|
||||
logsPanelContainer.setState({
|
||||
body: new NoRelatedLogsScene({}),
|
||||
});
|
||||
@ -88,12 +84,11 @@ export class RelatedLogsScene extends SceneObjectBase<RelatedLogsSceneState> {
|
||||
new CustomVariable({
|
||||
name: VAR_LOGS_DATASOURCE,
|
||||
label: 'Logs data source',
|
||||
query: lokiDatasources?.map((ds) => `${ds.name} : ${ds.uid}`).join(','),
|
||||
query: lokiDataSources?.map((ds) => `${ds.name} : ${ds.uid}`).join(','),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
controls: [new VariableValueSelectors({ layout: 'vertical' })],
|
||||
lokiRecordingRules,
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -107,11 +102,9 @@ export class RelatedLogsScene extends SceneObjectBase<RelatedLogsSceneState> {
|
||||
if (name === VAR_LOGS_DATASOURCE) {
|
||||
const selectedMetric = sceneGraph.interpolate(this, VAR_METRIC_EXPR);
|
||||
const selectedDatasourceUid = sceneGraph.interpolate(this, VAR_LOGS_DATASOURCE_EXPR);
|
||||
const lokiQuery = getLokiQueryForRelatedMetric(
|
||||
selectedMetric,
|
||||
selectedDatasourceUid,
|
||||
this.state.lokiRecordingRules
|
||||
);
|
||||
const lokiQuery = this.state.connectors
|
||||
.map((connector) => `(${connector.getLokiQueryExpr(selectedMetric, selectedDatasourceUid)})`)
|
||||
.join(' and ');
|
||||
|
||||
if (lokiQuery) {
|
||||
const relatedLogsQuery = sceneGraph.findByKeyAndType(this, RELATED_LOGS_QUERY_KEY, SceneQueryRunner);
|
||||
|
Loading…
Reference in New Issue
Block a user