diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index c84e1869072..ea9b8d2a4f4 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -536,82 +536,6 @@ describe('Prometheus Language Provider', () => { }); }); - describe('Error handling and metadata', () => { - it('should handle disabled metadata lookups gracefully', async () => { - const datasource: PrometheusDatasource = { - ...defaultDatasource, - metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })), - lookupsDisabled: false, - } as unknown as PrometheusDatasource; - const mockedMetadataRequest = jest.mocked(datasource.metadataRequest); - const instance = new PrometheusLanguageProvider(datasource); - - expect(mockedMetadataRequest.mock.calls.length).toBe(0); - await instance.start(); - expect(mockedMetadataRequest.mock.calls.length).toBeGreaterThan(0); - }); - - it('should handle metadata request failures gracefully', async () => { - jest.spyOn(console, 'error').mockImplementation(); - const datasource: PrometheusDatasource = { - ...defaultDatasource, - metadataRequest: jest.fn(() => Promise.reject('rejected')), - lookupsDisabled: false, - } as unknown as PrometheusDatasource; - const mockedMetadataRequest = jest.mocked(datasource.metadataRequest); - const instance = new PrometheusLanguageProvider(datasource); - - expect(mockedMetadataRequest.mock.calls.length).toBe(0); - const result = await instance.start(); - expect(result[0]).toBeUndefined(); - expect(result[1]).toEqual([]); - expect(mockedMetadataRequest.mock.calls.length).toBe(3); - }); - - it('should include cache headers for requests when cacheLevel is set', () => { - const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium); - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - cacheLevel: PrometheusCacheLevel.Medium, - } as PrometheusDatasource); - const fetchLabelValues = languageProvider.fetchLabelValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchLabelValues(getMockTimeRange(), 'job'); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][2]).toEqual({ - headers: { 'X-Grafana-Cache': `private, max-age=${timeSnapMinutes * 60}` }, - }); - }); - - it('should handle request errors gracefully', async () => { - jest.spyOn(console, 'error').mockImplementation(); - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const datasourceRequestMock = jest - .spyOn(defaultDatasource, 'metadataRequest') - .mockRejectedValue(new Error('Network error')); - - const result = await languageProvider.request('/api/v1/labels', {}); - - expect(datasourceRequestMock).toHaveBeenCalled(); - expect(result).toEqual(undefined); - }); - - it('should ignore cancelled request errors', async () => { - jest.spyOn(console, 'error').mockImplementation(); - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const error = { cancelled: true }; - const datasourceRequestMock = jest.spyOn(defaultDatasource, 'metadataRequest').mockRejectedValue(error); - - const result = await languageProvider.request('/api/v1/labels', {}); - - expect(datasourceRequestMock).toHaveBeenCalled(); - expect(result).toEqual(undefined); - expect(console.error).not.toHaveBeenCalled(); - }); - }); - describe('fetchSuggestions', () => { it('should send POST request with correct parameters', async () => { const timeRange = getMockTimeRange(); @@ -694,171 +618,6 @@ describe('Prometheus Language Provider', () => { ); }); }); - - describe('New PrometheusLanguageProvider Interface', () => { - let provider: PrometheusLanguageProviderInterface; - - beforeEach(() => { - provider = new PrometheusLanguageProvider(defaultDatasource); - }); - - describe('Retrieve Methods (Synchronous)', () => { - beforeEach(async () => { - // Setup the data through public methods - // This is the raw API response shape - const rawMetadata = { - metric1: [ - { - type: 'counter', - help: 'help text', - }, - ], - }; - // Mock the request to return raw API shape - jest.spyOn(provider, 'request').mockResolvedValue(rawMetadata); - await provider.queryMetricsMetadata(); - - // Setup metrics through start method - const metrics = ['metric1', 'metric2']; - jest.spyOn(provider, 'request').mockResolvedValue(metrics); - await provider.start(); - }); - - it('should retrieve metrics metadata', () => { - const result = provider.retrieveMetricsMetadata(); - expect(result).toEqual( - expect.objectContaining({ - metric1: { - type: 'counter', - help: 'help text', - }, - }) - ); - }); - - it('should retrieve histogram metrics', () => { - const result = provider.retrieveHistogramMetrics(); - expect(result).toEqual(expect.any(Array)); - }); - - it('should retrieve metrics', () => { - const result = provider.retrieveMetrics(); - expect(result).toEqual(expect.any(Array)); - }); - - it('should retrieve label keys', () => { - const result = provider.retrieveLabelKeys(); - expect(result).toEqual(expect.any(Array)); - }); - }); - - describe('Query Methods (Asynchronous)', () => { - const timeRange = getMockTimeRange(); - - it('should query metrics metadata', async () => { - // Raw API response shape - const rawMetadata = { - metric1: [ - { - type: 'counter', - help: 'help text', - }, - ], - }; - jest.spyOn(provider, 'request').mockResolvedValue(rawMetadata); - - const result = await provider.queryMetricsMetadata(); - - expect(result).toEqual( - expect.objectContaining({ - metric1: { - type: 'counter', - help: 'help text', - }, - }) - ); - }); - - describe('queryLabelKeys', () => { - it('should use labels API when supported', async () => { - const labelKeys = ['label1', 'label2']; - const datasourceWithLabelsAPI = { - ...defaultDatasource, - hasLabelsMatchAPISupport: () => true, - } as unknown as PrometheusDatasource; - provider = new PrometheusLanguageProvider(datasourceWithLabelsAPI); - jest.spyOn(provider, 'request').mockResolvedValue(labelKeys); - - const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}'); - - expect(result).toEqual(['label1', 'label2']); - }); - - it('should use series API when labels API not supported', async () => { - const series = [{ __name__: 'metric', label1: 'value1', label2: 'value2' }]; - const datasourceWithoutLabelsAPI = { - ...defaultDatasource, - hasLabelsMatchAPISupport: () => false, - } as unknown as PrometheusDatasource; - provider = new PrometheusLanguageProvider(datasourceWithoutLabelsAPI); - jest.spyOn(provider, 'request').mockResolvedValue(series); - - const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}'); - - expect(result).toEqual(['label1', 'label2']); - }); - - it('should handle empty series response', async () => { - jest.spyOn(provider, 'request').mockResolvedValue([]); - - const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}'); - - expect(result).toEqual([]); - }); - }); - - describe('queryLabelValues', () => { - it('should use labels API when supported', async () => { - const labelValues = ['value1', 'value2']; - const datasourceWithLabelsAPI = { - ...defaultDatasource, - hasLabelsMatchAPISupport: () => true, - } as unknown as PrometheusDatasource; - provider = new PrometheusLanguageProvider(datasourceWithLabelsAPI); - jest.spyOn(provider, 'request').mockResolvedValue(labelValues); - - const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}'); - - expect(result).toEqual(['value1', 'value2']); - }); - - it('should use series API when labels API not supported', async () => { - const series = [ - { __name__: 'metric', job: 'value1' }, - { __name__: 'metric', job: 'value2' }, - ]; - const datasourceWithoutLabelsAPI = { - ...defaultDatasource, - hasLabelsMatchAPISupport: () => false, - } as unknown as PrometheusDatasource; - provider = new PrometheusLanguageProvider(datasourceWithoutLabelsAPI); - jest.spyOn(provider, 'request').mockResolvedValue(series); - - const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}'); - - expect(result).toEqual(['value1', 'value2']); - }); - - it('should handle empty series response', async () => { - jest.spyOn(provider, 'request').mockResolvedValue([]); - - const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}'); - - expect(result).toEqual([]); - }); - }); - }); - }); }); describe('Query transformation', () => { diff --git a/packages/grafana-prometheus/src/resource_clients.test.ts b/packages/grafana-prometheus/src/resource_clients.test.ts index 7e7e2c36360..471e274c388 100644 --- a/packages/grafana-prometheus/src/resource_clients.test.ts +++ b/packages/grafana-prometheus/src/resource_clients.test.ts @@ -1,4 +1,213 @@ -import { processSeries } from './resource_clients'; +import { dateTime, TimeRange } from '@grafana/data'; + +import { LabelsApiClient, processSeries, SeriesApiClient } from './resource_clients'; +import { PrometheusCacheLevel } from './types'; + +const mockTimeRange: TimeRange = { + from: dateTime(1681300292392), + to: dateTime(1681300293392), + raw: { + from: 'now-1s', + to: 'now', + }, +}; + +const mockRequest = jest.fn(); +const mockGetAdjustedInterval = jest.fn().mockReturnValue({ + start: '1681300260', + end: '1681300320', +}); +const mockGetTimeRangeParams = jest.fn().mockReturnValue({ + start: '1681300260', + end: '1681300320', +}); +const mockInterpolateString = jest.fn((str) => str); +const defaultCacheHeaders = { headers: { 'X-Grafana-Cache': 'private, max-age=60' } }; + +describe('LabelsApiClient', () => { + let client: LabelsApiClient; + + beforeEach(() => { + jest.clearAllMocks(); + client = new LabelsApiClient( + mockRequest, + PrometheusCacheLevel.Low, + mockGetAdjustedInterval, + mockGetTimeRangeParams, + mockInterpolateString + ); + }); + + describe('start', () => { + it('should initialize metrics and label keys', async () => { + mockRequest.mockResolvedValueOnce(['metric1', 'metric2']).mockResolvedValueOnce(['label1', 'label2']); + + await client.start(mockTimeRange); + + expect(client.metrics).toEqual(['metric1', 'metric2']); + expect(client.labelKeys).toEqual(['label1', 'label2']); + }); + }); + + describe('queryMetrics', () => { + it('should fetch metrics and process histogram metrics', async () => { + mockRequest.mockResolvedValueOnce(['metric1_bucket', 'metric2_sum', 'metric3_count']); + + const result = await client.queryMetrics(mockTimeRange); + + expect(result.metrics).toEqual(['metric1_bucket', 'metric2_sum', 'metric3_count']); + expect(result.histogramMetrics).toEqual(['metric1_bucket']); + }); + }); + + describe('queryLabelKeys', () => { + it('should fetch and sort label keys', async () => { + mockRequest.mockResolvedValueOnce(['label2', 'label1', 'label3']); + + const result = await client.queryLabelKeys(mockTimeRange); + + expect(result).toEqual(['label1', 'label2', 'label3']); + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/labels', + { + limit: '40000', + start: expect.any(String), + end: expect.any(String), + }, + defaultCacheHeaders + ); + }); + + it('should include match parameter when provided', async () => { + mockRequest.mockResolvedValueOnce(['label1', 'label2']); + + await client.queryLabelKeys(mockTimeRange, '{job="grafana"}'); + + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/labels', + { + 'match[]': '{job="grafana"}', + limit: '40000', + start: expect.any(String), + end: expect.any(String), + }, + defaultCacheHeaders + ); + }); + }); + + describe('queryLabelValues', () => { + it('should fetch label values with proper encoding', async () => { + mockRequest.mockResolvedValueOnce(['value1', 'value2']); + mockInterpolateString.mockImplementationOnce((str) => str); + + const result = await client.queryLabelValues(mockTimeRange, 'job'); + + expect(result).toEqual(['value1', 'value2']); + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/label/job/values', + { + start: expect.any(String), + end: expect.any(String), + limit: '40000', + }, + defaultCacheHeaders + ); + }); + + it('should handle UTF-8 label names', async () => { + mockRequest.mockResolvedValueOnce(['value1', 'value2']); + mockInterpolateString.mockImplementationOnce((str) => 'http.status:sum'); + + await client.queryLabelValues(mockTimeRange, '"http.status:sum"'); + + expect(mockRequest).toHaveBeenCalledWith( + '/api/v1/label/U__http_2e_status:sum/values', + { + start: expect.any(String), + end: expect.any(String), + limit: '40000', + }, + defaultCacheHeaders + ); + }); + }); +}); + +describe('SeriesApiClient', () => { + let client: SeriesApiClient; + + beforeEach(() => { + jest.clearAllMocks(); + client = new SeriesApiClient( + mockRequest, + PrometheusCacheLevel.Low, + mockGetAdjustedInterval, + mockGetTimeRangeParams, + mockInterpolateString + ); + }); + + describe('start', () => { + it('should initialize metrics and histogram metrics', async () => { + mockRequest.mockResolvedValueOnce([{ __name__: 'metric1_bucket' }, { __name__: 'metric2_sum' }]); + + await client.start(mockTimeRange); + + expect(client.metrics).toEqual(['metric1_bucket', 'metric2_sum']); + expect(client.histogramMetrics).toEqual(['metric1_bucket']); + }); + }); + + describe('queryMetrics', () => { + it('should fetch and process series data', async () => { + mockRequest.mockResolvedValueOnce([ + { __name__: 'metric1', label1: 'value1' }, + { __name__: 'metric2', label2: 'value2' }, + ]); + + const result = await client.queryMetrics(mockTimeRange); + + expect(result.metrics).toEqual(['metric1', 'metric2']); + expect(client.labelKeys).toEqual(['label1', 'label2']); + }); + }); + + describe('queryLabelKeys', () => { + it('should throw error if match parameter is not provided', async () => { + await expect(client.queryLabelKeys(mockTimeRange)).rejects.toThrow( + 'Series endpoint always expects at least one matcher' + ); + }); + + it('should fetch and process label keys from series', async () => { + mockRequest.mockResolvedValueOnce([{ __name__: 'metric1', label1: 'value1', label2: 'value2' }]); + + const result = await client.queryLabelKeys(mockTimeRange, '{job="grafana"}'); + + expect(result).toEqual(['label1', 'label2']); + }); + }); + + describe('queryLabelValues', () => { + it('should throw error if match parameter is not provided', async () => { + await expect(client.queryLabelValues(mockTimeRange, 'job')).rejects.toThrow( + 'Series endpoint always expects at least one matcher' + ); + }); + + it('should fetch and process label values from series', async () => { + mockRequest.mockResolvedValueOnce([ + { __name__: 'metric1', job: 'grafana' }, + { __name__: 'metric2', job: 'prometheus' }, + ]); + + const result = await client.queryLabelValues(mockTimeRange, 'job', '{__name__="metric1"}'); + + expect(result).toEqual(['grafana', 'prometheus']); + }); + }); +}); describe('processSeries', () => { it('should extract metrics and label keys from series data', () => {